java中的偏序关系
从半个多月前接到阿里的面试电话,被多线程问题难住,到今天终于读完了《Java Concurrency In Practice》。想总结一下,又发现自己没有能力将一本书的内容都概括下来。还是把书里最后一部分Java内存模型相关的内容搬过来谈一谈吧。
1、happens-before
什么是Java内存模型?它是一个协议,规定了在怎样的条件下,一个线程对内存的操作对另一个线程是可见的。
例如:线程A将变量 variable 赋值为3,那在怎样的情况下,线程B中条件(variable == 3) 成立呢?多线程环境下有很多因素可以令其不成立,有可能新值仅储存在寄存器里,还有可能新值仅写入了线程A所在处理器的本地Cache,而没有写入主存。
什么是happens-before?它是Java定义的一种偏序关系,为了保证线程A执行的操作对线程B是可见的,A与B之间必须存在这种偏序关系。它有如下的规则:
- 程序顺序规则:一个线程中的每个操作都happens-before那个线程随后的操作。
- 监视器锁规则:一个线程释放监视器锁之前所有的操作都happens-before另一个线程获取同一个监视器锁随后的操作。
- volatile变量规则:一个线程写volatile变量之前的所有操作都hanppens-before另一个线程读那一个volatile变量随后的操作。
- 线程启动规则:一个线程调用 Thread.start() 之前所有的操作都happens-before被启动的线程中的所有操作。
- 线程终止规则:一个线程中的所有操作都happens-before其它发现该线程已经终止的线程随后的操作。
- 中断规则:一个线程使用 Thread.interrupt() 中断另一个线程前的所有操作都happens-before被中断的线程检测到该中断随后的操作。
- Finalizer规则:一个对象构造函数中的所有操作都happens-before该对象的 finalize() 函数被调用后的操作。
- 传递规则:如果A happens-before B, B happens-before C,那么 A happens-before C。
上图展示了两个线程在使用同一把锁进行线程同步时存在的偏序关系,需要注意的是必须是同一把锁才会存在偏序关系。
2、Double-checked Locking
在单例模式中使用懒加载是比较常见的手段,可以尽可能地减少应用的启动时间。一般来说使用懒加载都需要进行线程同步,早期的Java虚拟机在加锁时有较大的开销,导致许多开发者在编程时会尽量避免加锁。DCL的目标就是既可以实现懒加载又能避免加锁。
public class DoubleCheckedLocking { // 正确写法:private static volatile Resource resource; private static Resource resource; public static Resource getInstance() { if (resource == null) { synchronized (DoubleCheckedLocking.class) { if (resource == null) resource = new Resource(); } } return resource; } }
首先,程序先判断 resource 是否为空,当其值为空时进入同步块,然后再次判断其值是否为空,因为另一个线程可能在本线程进入同步块之前已经对其进行了初始化。
这段熟悉的代码实际上存在着一个名为“安全发布”的问题,就是说当一个线程发现 resource 的值不为空时 Resource 实例中的变量对该线程可能是不可见的。因为根据偏序规则,一个线程释放锁前的所有操作只保证对另一个获取同一个锁的线程随后的操作可见。解决这个问题需要使用 volatile 对 resource 进行修饰,因为根据前面的规则,一个线程写volatile变量前的所有操作,对另一个线程读同一个volatile变量随后的操作都是可见的。
对于现代的虚拟机来说,在竞争比较小的情况下加锁操作是很快的,所以这种双重检查发挥的作用已经大大被减弱了,使用静态内部类完成懒加载是一种比较好的选择。
public class ResourceFactory { private static class ResourceHolder { public final static Resource resource = new Resource(); } public static Resource getResource() { return ResourceHolder.resource; } }
3、衍生规则
JDK提供的类中也存在一些偏序关系:
- 一个线程将元素放入线程安全的集合前的所有操作happens-before另一个线程将其从集合中取出后的操作。
- 一个线程CountDownLatch.countDown() 前的操作happens-before另一个线程 CountDownLatch.await() 后的操作。
- 一个线程 Semaphore.release() 前的操作happens-before另一个线程 Semaphore.acquire() 后的操作。
- 被Future代表的任务在线程中执行的操作happens-before另一个线程Future.get() 后的操作。
- 一个线程向Executor提交Runnable或Callable前的操作happens-before该任务运行时所在线程的操作。
- 一个线程在CyclicBarrier.await() 前的操作happens-before所有被该 CyclicBarrier 释放进程的操作。如果启用了BarrierAction,那一个线程在 CyclicBarrier.await() 前的操作happens-before BarrierAction中的操作。BarrierAction中的操作happens-before被该 CyclicBarrier 释放进程的操作。