从DCL问题出发认识并发环境下的Java内存访问

来源:互联网 发布:淘宝网怎么退出智能版 编辑:程序博客网 时间:2024/06/04 18:12

字典

缩写全称DCLDouble-checked lockingJMMJava memory model

1. 什么是DCL问题

DCL的全称为Double-checked locking。Double-checked locking是为了在并发环境下减少锁操作,加快计算速度而诞生的“双检测锁”机制。外层检测机制用于检测“是否有必要进行锁操作”,内层检测机制用于检测“锁内部的逻辑是否应该发生”。

这样讲或许还不是很能让人明白,用几段实例程序来表达就会清楚很多:


1)(从远的简单扯一下)首先先从为什么要加锁开始讲起。

 Single-checked without locking示例代码:

/** * Single-checked without locking locking pattern *  * @author yongqing_wang */public class SingleCheckedNoLocking {    private static SingleCheckedNoLocking instance = null;    /**     * If instance is null, initialize it and then get the new instance     *      * @return     */    public static SingleCheckedNoLocking getInstance() {        if (instance == null) {            instance = new SingleCheckedNoLocking();        }        return instance;    }    public SingleCheckedNoLocking() {        instance = new SingleCheckedNoLocking();    }}

来看下这种无锁模式在并发情况下可能产生的问题:

     

图1. Single-checked without locking 问题展现

在并发环境下,如果两个线程对getInstance()的调用及发生关系如图1(左)所示,那么线程1在step 4所获得的实例其实为线程2创建的实例instance。

又或者,两个线程对getInstance()的调用及发生关系如图1(右)所示,那么线程2在step 4很有可能会获得一个还未实例化完成的对象(new SingleCheckedNoLocking()正在初始化过程中,但已获得instance的对象指针,因此instance不为null,具体一个类的初始化过程请参看其他关于Java的参考资料),此时返回的instance极有可能会发生空指针访问错误。


2)加锁后

因此在并发环境下需要加入“锁机制”,在“锁”的范围内仅允许相同的操作由一个线程发生,用以防止以上问题的发生。

Single-checked locking示例代码:

/** * Single-checked locking pattern *  * @author yongqing_wang */public class SingleCheckedLocking {    private static SingleCheckedLocking instance = null;    /**     * If instance is null, initialize it and then get the new instance     *      * @return     */    public static SingleCheckedLocking getInstance() {        synchronized (SingleCheckedLocking.class) {            if (instance == null) {                instance = new SingleCheckedLocking();            }        }        return instance;    }    public SingleCheckedLocking() {        instance = new SingleCheckedLocking();    }}

加锁后,所有的线程在调用getInstance()时必须逐一的进行竞争锁、加锁,锁内逻辑判断及操作、解锁。这样会导致并发程序的执行效率大大下降,因为此时涉及到锁竞争的操作会使得并发操作串行执行。


3)利用Double-checked locking进行提速

Double-checked locking示例代码:

/** * Double-checked locking pattern *  * @author yongqing_wang */public class DoubleCheckedLocking {    private static DoubleCheckedLocking instance = null;    /**     * If instance is null, initialize it and then get the new instance     *      * @return     */    public static DoubleCheckedLocking getInstance() {        if (null == instance) {            synchronized (DoubleCheckedLocking.class) {                if (null == instance) {                    instance = new DoubleCheckedLocking();                }            }        }        return instance;    }    public DoubleCheckedLocking() {        instance = new DoubleCheckedLocking();    }}

在double-checked中外层检测机制用于检测“是否有必要进行锁操作”这样就不会造成并发程序串行执行的问题。内层检测机制用于检测“锁内部的逻辑是否应该发生”,这样就不会造成重复初始化操作。是不是问题就这样解决了呢?先留下一个悬念,答案从后面的文字中去一一解开。


2. 并发环境下必须遵守的三条准则

讨论并发,必须先从并发环境下需要考虑的问题出发,有需求才有解决的方法,并发环境下必须要遵守三条准则:

1. 原子性(Atomicity)

在这里,原子性指的是某种操作的最小单位,其在操作过程中不允许被其他同类操作打断,以保证其“原子”特性。例如,在第一节中提到的synchronized关键字就是保证synchronized代码块,在多个线程中执行原子性的一种方式。

2. 可见性(Visibility)

可见性指的是当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。

3. 有序性(Ordering)

(这里我们特指线程间)有序性指在多个线程间需要有序执行的代码块能够串行执行,不会因为并发导致类似图1中的问题。


3. 并发环境下的挑战

不细细剖析三条准则为何而来,这三条准则必然是实践过程中的经验所得,并非空穴来风和胡乱YY而致。因此,再多做一回“拿来主义者”,且不质疑三条准则认为已知的三条准则能够确保并发准确的情况下,所面临的挑战。
1. 挑战:保证有序性
在第一节提到的DCL代码中,是否就一定能保证有序性呢?


图2. DCL中对于原子性的挑战

图2 是第一节中所提到的DCL代码块被两个线程执行的可能顺序。当线程1执行至step 2时,因为线程1极有可能还未真正初始化完毕,但此时instance已有其对象指针。因此,在step 3中,线程2认为instance非空,并立刻跳转至step 4返回一个未被初始化完全的instance实例,从而造成空指针访问异常。可见,此时多线程间的代码块并未按照编程者的意图有序执行,JVM无法保证在类的初始化过程中能够先初始化完毕类的实例对象再返回对象指针。

那么,要解决这个问题,需要借助一个辅助对象“指引”JVM有序执行代码块。经过改造后的DCL代码为:

解决DCL中的有序性问题代码:

/** * <h5>Double-checked locking pattern</h5> *  * <pre> * To deal with the ordering problem * </pre> *  * @author yongqing_wang */public class Singleton {    private static Singleton instance;    public static Singleton getInstance() {        Singleton helper = instance;        if (null == helper) {            synchronized (Singleton.class) {                if (null == helper) {                    instance = new Singleton();                    helper = instance;                }            }        }        return instance;    }}

helper对象是instance对象“判断替身”,所有的检测过程由instance对象的替身helper对象完成,instance看上去也只能在初始化完毕后才能将其“托付”给helper对象。因此,其初始化的有序过程似乎就被保证了。

2. 挑战:保证原子性

刚才的代码内其解决问题的基础是假定helper=instance必定发生在instance=new Singleton()之后,那么事实是否如此呢?不是!计算机为了保证指令执行的高效,会将部分指令重排后执行。也就是说尽管JVM规定了程序代码按照书写的先后顺序按控制流顺序发生(考虑分支、循环等操作),但具体到CPU执行时并不是这样的。计算机会在允许的范围内进行指令重排,从而导致helper=instance可能会先于instance的实例化完毕后发生。即,指令的执行无法保证instance=new Singleton()的原子性。

因此,JVM提供了volatile关键词用于解决指令重排而导致的原子性无法保证的问题:

解决原子性问题代码:

/** * <h5>Double-checked locking pattern</h5> *  * <pre> * To deal with the atomicity problem * </pre> *  * @author yongqing_wang */public class Singleton {    private volatile static Singleton instance;    public static Singleton getInstance() throws InterruptedException {        Singleton helper = instance;        if (null == helper) {            synchronized (Singleton.class) {                if (null == helper) {                    instance = new Singleton();                    helper = instance;                }            }        }        return instance;    }}

volatile的语义定义在《Java Virtual Machine Specification--Second Edition》中:

1)A use operation by T on V is permitted only if the previous operation by T on V was load, and a load operation by T on V is permitted only if the next operation by T on V is use. The use operation is said to be "associated" with the read operation that corresponds to the load.
2)A store operation by T on V is permitted only if the previous operation by T on V was assign, and an assign operation by T on V is permitted only if the next operation by T on V is store. The assign operation is said to be "associated" with the write operation that corresponds to the store.
3)Let action A be a use or assign by thread T on variable V, let action F be the load or store associated with A, and let action P be the read or write of V that corresponds to F. Similarly, let action B be a use or assign by thread T on variable W, let action G be the load or store associated with B, and let action Q be the read or write of W that corresponds to G. If A precedes B, then P must precede Q. (Less formally: operations on the master copies of volatile variables on behalf of a thread are performed by the main memory in exactly the order that the thread requested.)

(懒得翻译了)其大致含义总结归纳并对应我们的问题就是:保证了helper=instance只能发生于instance=new Singleton()后,所有被标注为volatile的变量,就像是一堵“屏障”保证了其前、后代码无法颠倒执行。


4. 总结

至此,DCL的问题大抵就结束了。但是,似乎我们还没有提过任何关于可见性的问题。那是因为,可见性的问题在JMM中已经利用Happen-Before原则进行保证,在DCL问题中很难表现出来。Happen-Before原则规定了JMM中八种操作的执行顺序,即lock, unlock, read, load, use, assign, store, write的执行顺序。在文献4中会有一定的涉及,可做扩展阅读。

任何一个简单的问题,在并发环境下其问题一般会变得复杂,并且对所考虑问题也需要更加的深入、细致。路漫漫兮,唯能hold住者笑傲江湖。共勉共勉~


参考文献:

[1] Double-checked locking问题介绍 from wiki -- http://en.wikipedia.org/wiki/Double-checked_locking

[2] 《深入理解Java虚拟机》

[3] JVM Specification(JVM规范) -- http://java.sun.com/docs/books/jvms/second_edition/html/VMSpecTOC.doc.html

[4] 三问JMM by Xiaorong -- http://download.csdn.net/detail/casia_wyq/3937957

	
				
		
原创粉丝点击