Java Concurrency in Practice

Reference: Goetz, Brian, and Tim Peierls. Java concurrency in practice. Pearson Education, 2006.

1. 简介

PASS

2. 线程安全性

  • 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
  • 无状态对象一定是线程安全的。
  • 由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。“冲入”意味着获取锁的操作的粒度是“线程”,而不是“调用”。

3. 对象的共享

  • Java内存模型要求,变量的读取操作和写入操作都必须是原子操作,但对于非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分解为两个32位的操作。
  • 加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。
  • volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

4. 对象的组合

PASS

5. 构建基础模块

  • 同步容器:Vector、Hashtable
  • 并发容器:ConcurrentHashMap、ConcurrentMap、CopyOnWriteArrayList
  • 阻塞队列:LinkedBlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue、SynchronousQueue
  • 处理对中断的响应:(1)传递InterruptedException;(2)恢复中断:调用当前线程上的interrupt方法。
  • 同步工具类:闭锁(CountDownLatch)、FutureTask、信号量(Semaphore)、栅栏(Barrier)
  • 构建高效且可伸缩的结果缓存

6. 任务执行

  • Executor框架:执行策略、线程池(newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor、newScheduledThreadPool)
  • CompletionService:将Executor和BlockingQueue的功能融合在一起。

7. 取消与关闭

  • JVM并不能保证阻塞方法检测到中断的速度,但在实际情况中响应速度还是非常快的。
  • 对中断操作的正确理解是:它并不会真正地中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己(这些时刻也被称为取消点)。
  • 如果一个线程由于执行同步的Socket I/O或者等待获得内置锁而阻塞,那么中断请求只能设置线程的中断状态,除此之外没有其他任何作用。
  • 停止基于线程的服务:“毒丸”对象、shutdownNow
  • 处理非正常的线程终止:(1)要为线程池中的所有线程设置一个UncaughtExceptionHandler,需要为ThreadExecutor的构造函数提供一个Thread;(2)只有通过execute提交的任务,才能将它抛出的异常交给未捕获异常处理器,而通过submit提交的任务不行。
  • JVM关闭:(1)Shutdown Hook;(2)守护线程;(3)终结器(finalize,避免使用)

8. 线程池的使用

  • 设置线程池的大小
  • 配置ThreadPoolExecutor(线程的创建与销毁、管理队列任务、饱和策略、线程工厂)
  • 递归算法的并行化

9. 图形用户界面应用程序

  • 为什么GUI是单线程的
  • 短时间的GUI任务
  • 长时间的GUI任务(取消、进度标识和完成标识)

10. 避免活跃性危险

  • 死锁:锁顺序死锁、协作对象间的死锁、资源死锁)
  • 如果在调用某个方法时不需要持有锁,那么这种调用被称为开放调用(Open Call)。
  • 死锁的避免与判断:支持定时的锁、通过线程转储信息来分析死锁)
  • 其他活跃性危险:饥饿、槽糕的响应性、活锁

11. 性能与可伸缩性

  • 可伸缩性:当增加计算资源时(例如CPU、内存、存储容量或I/O带宽),程序的吞吐量或者处理能力能相应地增加。
  • Amdahl定律
  • 线程引入的开销:上下文切换、内存同步、阻塞
  • 自旋等待(Spin-Waiting):通过循环不断地尝试获取锁,直到成功
  • 在并发程序中,对可伸缩性的最主要威胁就是独占方式的资源锁
  • 减少锁的竞争的三种思路:(1)减少锁的持有时间;(2)降低锁的请求频率;(3)使用带有协调机制的独占锁,这些机制允许更高的并发性
  • 减少锁的竞争的方法:(1)缩小锁的范围(“快进快出”);(2)减小锁的粒度;(3)锁分段;(4)避免热点域;(5)替代独占锁(并发容器、读写锁、不可变对象、原子变量);(6)检测CPU的利用率;(7)不使用对象池
  • 减少上下文切换的开销
  • 常用程序命令:perfbar(CPU忙碌程度)、vmstat/perfmon(上下文切换次数、内核占用率)、iostat/perfmon(磁盘I/O)

12. 并发程序的测试

  • 安全性(不发生任何错误的行为)、活跃性(某个良好的行为终究会发生)
  • 正确性测试
  • 性能测试
  • 除非线程由于密集的同步需求而被持续阻塞,否则非公平的信号量通常能实现更好的吞吐量,而公平的信号量则实现更低的变动性。
  • 避免性能测试的陷阱:垃圾回收、动态编译、对代码路径的不真实采样、不真实的竞争程度、无用代码的消除
  • 其他测试方法:代码审查(Code Review)、静态分析工具(FindBugs)、面向方向的测试技术、分析与检测工具

13. 显式锁

PASS

14. 构建自定义的同步工具

  • AbstractQueuedSynchronizer

15. 原子变量与非阻塞同步机制

  • CompareAndSet(CAS)
  • 在大多数处理器上,在无竞争的锁获取和释放的“快速代码路径”上的开销,大约是CAS开销的两倍。
  • 在高度竞争的情况下,锁的性能超过原子变量的性能;在适中情况下,原子变量的性能更好。这是因为锁在发生竞争时会挂起线程,从而降低了CPU的使用率和共享内存总线上的同步通信量。
  • 非阻塞算法:非阻塞栈、非阻塞链表、原子的域更新器(AtomicReferenceFieldUpater,适用于频繁分配并且生命周期短暂的对象)、ABA问题(AtomicStampedReference和AtomicMarkableReference)。

16. Java内存模型

  • JVM规定了JVM必须遵循一组最小保证,这组保证规定了对变量的写入操作在何时将对于其他线程可见。
  • Happens-Before偏序关系:程序顺序规则、监视器锁规则、volatile变量规则、线程启动规则、线程结束规则、中断规则、终结器规则、传递性。
  • 安全初始化:延迟初始化占位(Holder)类模式
  • 双重检查加锁(DCL):已经被广泛废弃,因为促使其出现的驱动力(无竞争同步的执行速度很慢、JVM启动很慢)已经不复存在。
Written on June 5, 2017