1. 线程安全性
- 构建并发程序时, 必须正确的使用线程和锁.
- "共享"意味着变量可以由多个线程同时访问, 而"可变"则意味着变量的值在其生命周期内可以发生变化.
- 正确的编程方法: 先使代码正确运行, 然后再提高代码的速度.
- 线程安全:当多个线程访问某个类时, 这个类始终表现出正确的行为, 那么就称这个类为线程安全类.
- 竞态条件: 在并发编程中, 由于不恰当的执行时序而出现不正确的结果的情况.[常见类型: 先检查后执行]
- 延迟初始化的目的是将对象的初始化操作推迟到实际被使用时才进行, 同时确保值被初始化一次.
- 内置锁: java通过内置锁机制来支持原子性: 同步代码块(Synchronized Block) . 每个java对象都可以用作一个实现同步的锁, 这些锁称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock). 线程进入同步代码块之前会自动获取锁, 退出时释放锁. Java内置锁相当于一种互斥锁, 这意味着最多只有一个线程能只有这中锁.
- 重入: 当某个线程请求一个由其他线程持有的锁时, 发出的请求会阻塞. 而内置锁是可重入的, 如果某个线程视图获得一个已经由它自己(同一个线程 )持有的锁, 那么这个请求会成功. 当线程请求一个未被持有的锁时, JVM将记下锁的持有者, 并且将获取计数器置为1, 如果同一个线程再次获取这个锁, 计数器将递增, 退出时,计数器递减. 当计数器为0时, 这个锁被释放.
- 串行访问意味着多个线程依次以独占的方式访问对象, 而不是并发访问. 串行机制通常都无法提供高吞吐率或快速响应性.
2. 对象的共享
- 重排序: 在没有同步的情况下, 编译器, 处理器,以及运行时等都可以对操作的顺序进行一些意想不到的调整, 目的是提高性能.
- Java内存模型要求, 变量的读取和写入操作都必须是原子操作, 但对于非volatile类型的long和double变量, JVM允许将64位的读操作或写操作分解为两个32位的操作.
- Volatile变量: Java语言提供了一种稍弱的同步机制, 即Volatile变量, 用来确保将变量的更新操作通知到其他线程. volatile变量不会进行重排序, 不会被缓存到寄存器或者对其他处理器不可见的地方, 因此在读取volatile类型的变量时总会返回最新的写入值. 由于在访问volatile变量时不会执行加锁操作, 所以不会使执行的线程阻塞. volatile是比synchronized关键字更轻量级的机制. 从内存可见性的角度来看, 写入volatile变量相当于退出同步代码块, 读取相当于进入同步代码块.
- 使用volatile场景:
1. 当变量的写入操作不依赖变量的当前值, 或者能确保只有单个线程更新变量的值.
2. 该变量不会与其他状态变量一起纳入不变性条件中.
3. 在访问变量时不需要加锁. - ThreadLocal类: 维持线程封闭性更规范的方法是使用ThreadLocl, 这个类能使线程中的某个值与保存值关联起来. ThreadLocal提供了get和set等方法,这些方法为每个使用该变量的线程都保存一份独立的副本, 因此get总是返回由当前执行线程在调用set时设置的最新值. ThreadLocal对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享. 当某个线程初次调用ThreadLocal.get方法时, 就会调用initialValue来获取值. 这些特定于线程的值保存在Thread对象中, 当线程终止后, 这些值作为垃圾回收.
- Final域: final类型的域是不能改的(但如果final域所引用的对象是可变的, 那么这些被引用的对象可以是可修改的). 在java内存模型中, final域能确保初始化过程的安全性, 从而可以不受限制的访问不可变对象, 并在共享这些对象时无须公布.
3. 基础构建模块
- 同步容器类都是线程安全的, 它们通过其自身的锁来保护它的每个方法, 将所有对容器状态的访问都串行化, 降低了并发性.
- 持有锁的时间越长, 那么在锁上的竞争就可能越激烈, 如果许多线程都在等锁释放, 那么将极大地降低吞吐量和CPU的利用率.
- 编译器将字符串的连接操作转换为StringBuilder的apped(Object), 这个方法又会调用容器的toString方法, 标准容器的toString方法将迭代容器, 并在每个元素上调用toString来生成容器内容的格式化标识.
- Queue上的操作不会阻塞, 如果队列为空, 获取元素返回空值. BlockingQueue扩展了Queue, 增加了可阻塞的插入和获取等操作[队列满时阻塞插入, 空时阻塞获取].
- ConcurrentHashMap: 与HashMap一样, 是一个基于散列的Map, 但它使用完全不同的加锁策略(分段锁, Lock Striping. 16个段)来提供更高的并发性和伸缩性. 任意数量的读取线程可以并发地访问Map, 执行读操作的线程和执行写操作的线程可以并发地访问Map, 并且一定数据的写入线程可以并发地修改Map. 只有当应用程序需要加锁Map进行独占访问时, 才应该放弃使用ConCurrentHashMap.
- CopyOnWriteArrayList: 是一个线程安全的链表, 特别适用于管理监听器列表. 有良好的并发性, 并且在迭代期间不需要对容器进行加锁或复制. 每次修改时, 都会创建并重新发布一个新的容器副本, 从而实现可变性. 不会抛出ConcurrentModificationException. 仅当迭代操作远远多余修改操作时, 才应使用"写入时复制"容器.
- DeQue: 是一个双端队列, 实现了在队列头和队列尾的高效插入和移除. 具体实现有ArrayQueue和LinkBlockingDeque.
- 中断是一种协作机制. 最常使用中断的情况是取消某个操作. 方法对中断的请求响应速度越高, 就越容易及时取消那些执行时间很长的操作.
- 闭锁: 是一种同步工具类, 可以言辞线程的进度直到达到终止状态. CountDownLatch是一种灵活的闭锁实现, 它可以使一个或多个线程等待一组事件的发生. 计数器为0表示所有要等待的时间都已经发生, 非0会一直阻塞. FutureTask也可以做闭锁.
- 信号量: 计数信号量(Counting Semaphore)用来控制同时访问某个特定资源的操作数量, 或同时执行某个指定操作的数量. 计数信号量还可以用来实现某种资源池, 或者对容器加边界. Semaphore中管理者一组虚拟的许可(permit), 许可的初始数量可通过构造函数来指定. 计数信号量是一种简化形式的二值信号量, 即初始值为1的Semaphore. 二值信号量可以用做互斥体(mutex), 并具备不可重入的加锁语意: 谁拥有这个唯一的许可,谁就拥有了互斥锁.
- 栅栏(barrier): 类似于闭锁,它能阻塞一组线程知道某个事件发生. 区别在于: 所有线程必须同时达到栅栏位置, 才能继续执行. 闭锁用于等待事件, 而栅栏用于等待其他线程. CyclicBarrier: 可以使一定数量的参与方式反复地在栅栏位置汇集, 它在并行迭代算法中非常有用.
4.任务执行
- 任务: 通常是一些抽象的且离散的工作单元. 在理想情况下, 各个任务之间是相互独立的: 任务并不依赖于其他任务的状态、结果或者边界效应.
- Executor框架: 能支持多种不同的任务执行策略, 还提供了生命周期的支持, 以及统计信息收集, 应用程序管理机制和性能监控等机制.
- 线程池: 是指管理一组同构工作线程的资源池. 工作者线程(Worker Thread)的任务很简单: 从工作队列中获取一个任务, 执行任务, 然后返回线程池等待下一个任务. 通过重用现有的线程而不是创建新线程, 可以在处理多个请求时分摊线程创建和销毁过程中产生巨大的开销.
- Executors中的静态方法创建线程池:
1.newFixedThreadPool: 创建一个固定长度的线程池, 每当提交一个任务时就创建一个线程, 知道达到线程池的最大数量, 这是线程池规模不再变化(如果某个线程由于发生了未逾期的Exception异常而结束, 那么线程池会补充一个新的线程).
2. newCachedThreadPool: 创建一个可缓存的线程池, 如果线程池的当前规模超过了处理需求时, 将回收空闲线程, 需求增加时, 添加新的线程, 线程池的规模不存在任何限制.
3. newSingleThreadPool: 是一个单线程的Executor, 它创建单个工作者线程来执行任务, 如果线程异常结束, 会创建另外一个线程来替代. 能确保依照队列中的顺序来串行执行.
4. newScheduledThreadPool: 创建一个固定长度的线程池, 而且可以延迟或定时的方式来执行任务. - JVM只有在所有(非守护)线程全部终止后才会退出.
- ExecutorService的生命周期有3中状态: 运行、关闭和已终止.showdown方法将执行平缓的关闭过程: 不再接受新任务, 同时等待已提交的任务执行完成-包括那些未开始执行的任务. shutdownNow方法将执行粗暴的关闭过程: 它将尝试取消所有运行中的任务, 并且不再启动队列汇总未开始执行的任务.
- 如果要构建自己的调度任务, 可以使用DelayQueue, 它实现了BlockingQueue, 并且为ScheduledThreadPoolExecutor提供调度功能.
- Executor执行的任务有4个生命周期阶段: 创建、提交、开始和完成. 在Executor框架中, 已提交但尚未开始的任务可以取消, 但对于那些已经开始执行的任务, 只有当它们响应中断时, 才能取消.
- Future: 标识一个任务的生命周期, 并提供了响应的方法来判断是否已完成或取消, 以及获取任务的结果和取消任务等. 在Future规范中包含的意义是, 任务的生命周期只能前进, 不能后退, 就像ExecutorService的生命周期一样. 当任务完成后, 他就永远留在"完成"状态上.
- CompletionService(完成服务): 将Executor和BlockingQueue的功能融合在一起. 可以将Callable任务提交给它来执行, 人后使用类似于队列操作的take和poll等方法来获得已完成的结果, 而这些结果会在完成时被封装为Future. ExecutorCompletionService实现了CompletionService, 并将计算部分委托给一个Executor.
失效数据.缓存污染(Cache Pullution). 缓存逾期. 线程泄漏(thread Leakage)
入列[Enqueue]. 出列[Dequeue].
LinkedBlockingQueue和是FIFO队列, PriorityBlockingQueue是按优先级队列.
AtomicReference是一种替代对象引用的线程安全类.
委托是创建线程安全类的一个最有效的策略: 只需要让现有的线程安全类管理所有的状态即可.