透过DCL单例实现谈谈安全发布

来源:互联网 发布:终端写c语言 编辑:程序博客网 时间:2024/06/15 00:34

1.不安全的发布


   线程之间共享数据时,就是在“发布一个共享对象”与“另一个线程访问该对象”之间缺少一种Happens-Before关系(比如A、B两个线程,想要保证操作B的线程看到操作A的结果,那么A与B必须满足Happens-Before关系)时,就可能出现重排序问题。在没有充分同步的情况下,发布一个对象可能导致另一个线程看到一个只被部分构造的对象。

先来看个双重检查加锁(DCL)单例实现


/** * DCL单例 * @author renhj * */public class DoubleCheckedLocking {public static DoubleCheckedLocking instance;private DoubleCheckedLocking(){}public static DoubleCheckedLocking getInstance(){if(instance == null){synchronized(DoubleCheckedLocking.class){if(instance == null){instance = new DoubleCheckedLocking();}}}return instance;}public static void main(String[] args) {System.out.println(DoubleCheckedLocking.getInstance());System.out.println(DoubleCheckedLocking.getInstance());}}


instance = new DoubleCheckedLocking();这一行创建对象并不是一个原子操作,分为以下三步:
1)Allocate memory       //给DoubleCheckedLocking分配内存
2)invoke constructor   //DoubleCheckedLocking构造器实例化
3)give reference       //instance引用指向分配的内存地址 

由于Java编译器允许处理器乱序执行(out-of-order),上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是1-3-2,如果是后者,当前instance引用是一个部分构造的对象。


这种DCL实现方式很糟糕,首先检查是否在没有同步的情况下需要初始化,如果instance引用不为空,那么就直接使用它。否则就进行同步并检查Resource是否被初始化,从而保证只有一个线程对共享的DoubleCheckedLocking执行初始化。DCL的真正问题在于:没有同步的情况下读取一个共享对象时,可能发生的最糟糕的事情只是看到一个失效值(在这种情况下是个空值),此时DCL方法将通过在持有锁的情况下再次尝试来避免这种风险。然而实际情况远比这种糟糕,线程可能看到引用的当前值,但是对象的状态确是失效的(这种情况下是个部分构造的对象)


在Java5.0以后更高的版本中,如果把instance声明为volatile类型,对instance的写会强制将对缓存的修改操作立即写入主存,其他线程缓存中的instance都会设置为无效状态,从而确保共享内存中读到的instance是最新的,而且满足Happens_Before关系。


2.安全发布的常用模式


   可变对象必须通过安全的方式来发布,要安全的发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见,一个正确的构造对象可以通过以下方式来安全的发布:

  1. 在静态初始化函数中初始化一个对象的引用。

  2. 将对象的引用保存到一个volatile类型的域或者AtomicReferance对象中。

  3. 将对象的引用保存到一个正确构造对象的final类型域中。

  4. 将对象的引用保存到一个由锁保护的域中。

静态初始化器由JVM在类的初始化阶段执行,JVM内部存在着同步机制,因此通过这种方式初始化任何对象都可以被安全的发布,例如:public static DoubleCheckedLocking dcl = new DoubleCheckedLocking() ;volatile修饰的对象具有可见性,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值,从而确保对象的安全发布;final修饰的对象,那么该对象不可变,可以确保被安全的发布;锁确保可见性,对对象的写和读操作都上锁,能确保对象的安全发布。


附Happens-Before偏序关系一览:


1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
2. 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作。这里必须强调的是同一个锁,这里“后面”是时间上的先后顺序。
3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作,这里“后面”是时间上的先后顺序。
4. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作。
5. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否中断发生。
6. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。
7. 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于他的finalize()方法的开始。

8. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。



2 0