Java并发编程实战
Java 内存模型
Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括 volatile、synchronized 和 final 三个关键字,以及六项 Happens-Before 规则。
Happens-Before 规则
前面一个操作的结果对后续操作是可见的
Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens-Before 规则。
- 程序的顺序性规则
- volatile 变量规则
对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作。 - 传递性
- 管程中锁的规则
对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。 - 线程 start() 规则
主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。 - 线程 join() 规则
互斥锁-原子性问题
如果能够保证对共享变量的修改是互斥的,那么,无论是单核 CPU 还是多核 CPU,就都能保证原子性了。
临界区、锁:锁的是什么?我们保护的又是什么?
Java 提供的 synchronized 关键字,就是锁的一种实现。
- 当修饰静态方法的时候,锁定的是当前类的 Class 对象;
- 当修饰非静态方法的时候,锁定的是当前实例对象 this。
受保护资源和锁之间合理的关联关系应该是 N:1 的关系。
解决原子性问题,是要保证中间状态对外不可见。
使用细粒度锁可以提高并行度,是性能优化的一个重要手段。
死锁:一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。
只有以下这四个条件都发生时才会出现死锁:
- 互斥,共享资源 X 和 Y 只能被一个线程占用;
- 占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
- 不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
- 循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。
反过来分析,也就是只要破坏其中一个,就可以成功避免死锁的发生。
一个完整的等待-通知机制:线程首先获取互斥锁,当线程要求的条件不满足时,释放互斥锁,进入等待状态;当要求的条件满足时,通知等待的线程,重新获取互斥锁。
wait()和 sleep()的区别?
管程,指的是管理共享变量以及对共享变量的操作过程,让他们支持并发.
将硬件的性能发挥到极致
有三个方法 A、B、C,他们的调用关系是 A->B->C(A 调用 B,B 调用 C),在运行时,会构建出下面这样的调用栈。每个方法在调用栈里都有自己的独立空间,称为栈帧,每个栈帧里都有对应方法需要的参数和返回地址。当调用方法时,会创建新的栈帧,并压入调用栈;当方法返回时,对应的栈帧就会被自动弹出。也就是说,栈帧和方法是同生共死的。
调用栈与线程
两个线程可以同时用不同的参数调用相同的方法,那调用栈和线程之间是什么关系呢?答案是:每个线程都有自己独立的调用栈。
如何用面向对象思想写好并发程序?
一、封装共享变量:将共享变量作为对象属性封装在内部,对所有公共方法制定并发访问策略。
二、识别共享变量间的约束条件:一定要识别出所有共享变量之间的约束条件,如果约束条件识别不足,很可能导致制定的并发访问策略南辕北辙。共享变量之间的约束条件,反映在代码里,基本上都会有 if 语句,所以,一定要特别注意竞态条件。
三、制定并发访问策略:避免共享、不变模式、管程及其他同步工具。