总结java内存数据的可见性和dcl的根本问题原因

    本文主要内容为网络收集,经过加工处理,参考文章见附录。
    说到java的并发编程,就必须要了解在并发中的一些数据的读取,特别是关键性数据的读取。为防止由于多线程的访问对于数据的读取产生读取顺序上的不一致,java引入了内存模型的概念,其根本概念即happen-before规则。

    happen-before规定在jvm实现内部,各个运行中的变量的读取顺序以及它对于多个线程在读取上的可见性。因为,在多个线程进行同时操作时,变量会根据每个线程保存它自己的一个副本(类似于clone),并且在必要的时候同主存(即原始的数据)同步一次。正是因为有这样的概念,在多线程中,我们看到的对象的数据值,可能就不是最新的一个数据值,而是一个过期的数据,或者说是一个错误的数据。而基于想象中正确的逻辑进行编程的话,就可能出现一些问题。
    happens-before就是“什么什么一定在什么什么之前运行”,也就是保证顺序性。即在多个线程或者同一个线程的不同操作之间,要满足说哪个操作一定会在哪个操作之前发生。那么在相对靠后的操作中使用的变量所读取的值,则一定是在操作之前写入的数据,不会读取到还未写入的值。

    happen-before有以下几种保证规则(引自《java并发编程实践》)

  •         程序次序法则:线程中的每个动作A都happen-before于该线程中的每一个动作B,其中,在程序中,所有的动作B都出现在动作A之后。
  •         监视器锁法则:对一个监视器的解锁happen-before于每一个后续对同一个监视器的加锁。
  •         volatile变量法则:对volatile域的写入操作happen-before于每个后续对同一个域的读取操作。
  •         线程启动法则:在一个线程里,对Thread.start的调用会happen-before于每一个启动线程中的动作。
  •         线程终结法则:线程中的任何动作都happen-before于其它线程检测到这个线程已经终结,或者从Thread.join调用中成功返回,或者Thread.isAive返回false。
  •         中断法则:一个线程调用另一个线程的interrupt happen-before于被中断的线程发现中断(通过抛出InterruptedException,或者调用isInterrupted和interrupted)。
  •         终结法则:一个对象的构造函数的结束happen-before于这个对象finalizer的开始。
  •         传递性:如果C happen-before于B,且B happen-before于C,则A happen-before于C。

    happen-before中有一个重要的概念,就是happen-before只发生在不同的线程或变量访问之间。而对于一个方法的内部,happen-before却没有规定其的顺序规则。这样做的好处在于编译器优化或者cpu执行可以更加高效地执行代码,这其中很重要的一个就是cpu并发指令执行或者是指令乱序。
    很简单一个例子即是一个对象的初始化过程。它包括创建对象本身以及对对象内部的属性赋值至少两个以上的操作。从正常地来说,会先创建对象本身(1),数据赋值(2),外部引用传递(3)。但是由于乱序的存在,可能存在在整个过程中,第2步操作和第3步操作乱序执行的问题。此问题会直接导致dcl解决算法失败或者错误。

    所谓dcl,即double check lock,双重检查锁。通过两次检查和加锁来实现一个对象的懒加载。简单代码如下所示:

public static LazySingleton getInstance() {   
    if (m_instance == null) {   
        synchronized (LazySingleton.class) {   
            if (m_instance == null) {   
                m_instance = new LazySingleton();   
            }   
        }   
    }   
    return m_instance;   
}   

    以上的代码即是一个经典的dcl实现,那么我们为什么说这个初始化是错误的呢。原因就在于两个线程之间并没有明显地happen-before关系。即一个线程可能看到一个已经初始化的变量值,也可没有看到,更有可能看到一个正在初始化过程中的变量值(由于乱序的问题)。如果引用了一个正在初始化或者只初始化一半的变量,并修改了其中数据。这就会导致明显的程序错误了。

    解决方法有许多,比如将变量内部的外部可访问属性设置成final的,或者是volatile的。在java5版本中,对于final语意的增强,保证了其它线程看到这个变量的final字段时,只能是已经通过构造函数初始化好了的字段值,而不是示初始的值。此约定是由jvm规范制定的。

    了解多线程,对于了解jvm内部对于数据的处理方式,有一定的指导意义。
    参考文章列表:
    http://www.iteye.com/topic/157952    再请教双重检查锁定DCL的问题
    http://javatar.iteye.com/blog/144763    Java内存模型happens-before法则
    http://spiritfrog.iteye.com/blog/214986    由延时加载的单例模式引发的思考

转载请标明出处:i flym
本文地址:https://www.iflym.com/index.php/code/201108040001.html

相关文章:

作者: flym

I am flym,the master of the site:)

《总结java内存数据的可见性和dcl的根本问题原因》有一个想法

  1. 1.”原因就在于两个线程之间并没有明显地happen-before关系”,准确的说是在”if (m_instance == null)”无happen-before关系,但在”synchronized (LazySingleton.class)”是存在happen-before关系的。
    2.赞lz的这句话,”happen-before中有一个重要的概念,就是happen-before只发生在不同的线程或变量访问之间。而对于一个方法的内部,happen-before却没有规定其的顺序规则”,之前简单的看过jsr133(关于jmm定义),不是很明白。
    “解决方法有许多,比如将变量内部的外部可访问属性设置成final的,或者是volatile”,jmm不允许reorder。

发表评论

邮箱地址不会被公开。 必填项已用*标注