浅析 Java 内存模型 二
先行发生原则(Happens-Before)
先行发生是 Java 内存模型中定义的两项操作之间的偏序关系,比如说操作 A 先行发生于操作 B,其实就是说在发生操作 B 之前,操作 A 产生的影响能被操作 B 观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。
单线程规则
在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。注意,这里说的是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
锁操作(掌握)
一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。这 里必须强调的是“同一个锁”,而“后面”是指时间上的先后。
volatile 变量规则
对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后。
线程启动
Thread 对象的 start() 方法先行发生于此线程的每一个动作。
线程 join
线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过 Thread::join() 方法是否结束、Thread::isAlive() 的返回值等手段检测线程是否已经终止执行。
传递性
如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那就可以得出操作 A 先行发生于操作 C 的结论。
中断
对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread::interrupted() 方法检测到是否有中断发生。
构造方法
一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
工具类的 Happens-Before 原则
线程安全的容器 get 一定能看到在此之前的 put 等存入动作
CountDownLatch
Semaphore
Future
线程池
CyclicBarrier
Happens-before 实例演示
在上一小节中讲到有第四种情况(低概率)的发生:没给 b 加 volatile,那么有可能出现 `a = 1, b = 3`。因为 a 虽然被修改了,但是其他线程不可见,而 b 恰好其他线程可见,这就造成了 `a = 1, b = 3`。
若是给 b 加了 volatile,不仅 b 被影响,也可以实现轻量级同步,这就是所谓的“近朱者赤”。
b 之前的写入(对应 `b = a`)对读取 b 后的代码(print b)都可见,所以在 writerThread 里对 a 的赋值,一定会对 readerThread 里的读取可见,所以这里的 a 即使不加 volatile,只要读到的是 3,就可以有 happens-before 原则保证了读取到的都是 3 而不可能读取到 1。
原子性
什么是原子性
也就是一系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,是不可分割的。
Java 中的原子操作有哪些?
除 long 和 double 之外的基本数据类型(int、byte、boolean、short、char、float)的赋值操作。
所有引用(reference) 的赋值操作,不管是 32 位的机器还是 64 位的机器。
java.concurrent.Atomic.* 包下所有类的原子操作。
long 和 double 的原子性
结论:在 32 位上的 JVM 上,long 和 double 的操作不是原子的,但是在 64 位的 JVM 上是原子的
在实际中,在商用 JVM 中不会出现
原子操作 + 原子操作 != 原子操作
简单地把原子操作组合在一起,并不能保证整体依然具有原子性。
版权声明: 本文为 InfoQ 作者【朱华】的原创文章。
原文链接:【http://xie.infoq.cn/article/66de91a9eabf71b64a82525f1】。未经作者许可,禁止转载。
评论