从单例模式到HappensBefore

目录java

  • 双重检测锁的演变过程
  • 利用HappensBefore分析并发问题
  • 无volatile的双重检测锁

双重检测锁的演变过程

synchronized修饰方法的单例模式

双重检测锁的最初形态是经过在方法声明的部分加上synchronized进行同步,保证同一时间调用方法的线程只有一个,从而保证new Singlton()的线程安全:安全

public class Singleton {

    private static Singleton instance;

    private Singleton() {
    }

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

这样作的好处是代码简单、而且JVM保证new Singlton()这行代码线程安全。可是付出的代价有点高昂:
全部的线程的每一次调用都是同步调用,性能开销很大,并且new Singlton()只会执行一次,不须要每一次都进行同步。多线程

既然只须要在new Singlton()时进行同步,那么把synchronized的同步范围缩小呢?并发

线程不安全的双重检测锁

public class Singleton {

    private static Singleton instance;

    private Singleton() {
    }

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

synchronized同步的范围缩小之后,貌似是解决了每次调用都须要进行同步而致使的性能开销的问题。可是有引入了新的问题:线程不安全,返回的对象可能尚未初始化。app

深刻到字节码的层面来看看下面这段代码:性能

instance = new Singleton()
returen instance;

正常状况下JVM编译成成字节码,它是这样的:线程

step.1 new:开辟一块内存空间
step.2 invokespecial:执行初始化方法,对内存进行初始化
step.3 putstatic:将该内存空间的引用赋值给instance
step.4 areturn:方法执行结束,返回instance

固然这里限定在正常状况下,在特殊状况下也能够编译成这样:code

step.1 new:开辟一块内存空间
step.3 putstatic:将该内存空间的引用赋值给instance
step.2 invokespecial:执行初始化方法,对内存进行初始化
step.4 areturn:方法执行结束,返回instance

步骤2和步骤3进行了调换:先执行步骤3再执行步骤2。对象

  • 若是只有一个线程调用是没有问题的:由于无论步骤如何调换,JVM保证返回的对象是已经构造好了。
  • 若是同时有多个线程调用,那么部分调用线程返回的对象有多是没有构造好的对象。

这种特殊状况称之为:指令重排序:CPU采用了容许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理。固然不是乱排序,重排序保证CPU可以正确处理指令依赖状况以保障程序可以得出正确的执行结果。排序

利用HappensBefore分析并发问题

什么是HappensBefore

HappensBefore:先行发生,是

  • 判断数据是否存在竞争、线程是否安全的重要依据
  • A happens-beforeB,那么A对B可见(A作的操做对B可见)
  • 是一种偏序关系。hb(a,b),hb(b,c) => hb(a,c)

换句话说,能够经过HappensBefore推断代码在多线程下是否线程安全

举一个《深刻理解Java虚拟机》上的例子:

//如下操做在线程A中执行
int i = 1;

//如下操做在线程B中执行
j = i;

//如下操做在线程C中执行
i = 2;

若是hb(i=1,j=i),那么能够肯定变量j的值必定等于1。得出这个结论的依据有两个:

  1. 根据HappensBefore的规则,i=1的结果能够被j=i观察到
  2. 线程C尚未登场

若是线程C的执行时间在线程A和线程B之间,那么j的值是多少呢?答案是不肯定!由于线程C和线程B之间没有HappensBefore的关系:线程C对变量的i的更改可能被线程B观察到也可能不会!

HappensBefore关系

这些是“自然的”、JVM保证的HappensBefore关系:

  1. 程序次序规则
  2. 管程锁定规则
  3. volatile变量规则
  4. 线程启动规则
  5. 线程终止规则
  6. 线程中断规则
  7. 对象终结规则
  8. 传递性

重点介绍程序次序规则管程锁定规则volatile变量规则传递性,后面分析须要用到这四个性质:

  • 程序次序规则:在一个线程内,按照程序控制流顺序,书写在前面的操做HappensBefore书写在后面的操做
  • 管程锁定规则:对于同一个锁来讲,在时间顺序上,上一个unlock操做HappensBefore下一个lock操做
  • volatile变量规则:对于一个volatile修饰的变量,在时间顺序上,写操做HappensBefore读操做
  • 传递性:hb(a,b),hb(b,c) => hb(a,c)

分析以前线程不安全的双重检测锁

public class Singleton {

    private static Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {                     //1
            synchronized (Singleton.class) {        //2
                if (instance == null) {             //3
                    instance = new Singleton();     //4
                    new                             //4.1
                    invokespecial                   //4.2
                    pustatic                        //4.3
                }
            }
        }
        return instance;                            //5
    }
}

通过上面的讨论,已经知道由于JVM重排序致使代码4.2提早执行了,致使后面一个线程执行代码1返回的值为false,进而直接返回了尚未构造好的instance对象:

线程1 线程2
1
2
3
4.1
4.3
1
5
4.2
5

经过表格,可能清晰看到问题所在:线程1代码4.3 执行后,线程2执行代码1读到了脏数据。要想不读到脏数据,只要证实存在hb(T1-4.3,T2-1)(T1-4表示线程1代码4,T2-1表示线程2代码1,下同),那么是否存在呢?很遗憾,不存在:

  • 程序次序规则:不在同一个线程
  • 管程锁定规则:线程2没有尝试lock
  • volatile变量规则:instance对象没有经过volatile关键字修饰
  • 传递性:不存在

用HappensBefore分析,能够很清晰、明确看到没有volatile修饰的双重检测锁是线程不安全的。但,真的是这样的吗?

无volatile的双重检测锁

在第二部分,经过HappensBefore分析没有volatile修饰的双重检测锁是线程不安全,那只有用volatile修饰的双重检测锁才是线程安全的吗?答案是否认的。

用volatile关键字修饰的本质是想利用volatile变量规则,使得写操做(T1-4)HappensBefore读操做(T2-1),那只要另找一条HappensBefore规则保证便可。答案是程序次序规则管程锁定规则

先看代码:

public class Singleton {

    private static Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {                         //1
            synchronized (Singleton.class) {            //2
                if (instance == null) {                 //3
                    Singleton temp = new Singleton();   //4
                    temp.toString();                    //5
                    instance = temp;                    //6
                }
            }
        }
        return instance;                                //7
    }
}

在原有的基础上加了两行代码:

instance = new Singleton();           //4

Singleton temp = new Singleton();   //4
temp.toString();                    //5
instance = temp;                    //6

为何要这么作?
经过管程锁定规则保证执行到代码6时,temp对象已经构造好了。想想,为何?

  1. 其余线程执行代码1时,若是可以观察到T1-6的写操做,那么直接返回instance对象
  2. 若是没有观察到T1-6的写操做,那么尝试获取锁,此时管程锁定规则开始生效:保证当前线程必定可以观察到T1-6操做

执行流程多是这样的:

线程1 线程2 线程3
1
1
2
3
4
5
6
2
3
1 7
7
7

不管怎样执行,其余线程都可以观察到T1-6的写操做

其余

volatile、synchronized为何能够禁止JVM重排序

内存屏障。

JVM在凡有volatile、synchronized出现的地方都加了一道内存屏障:重排序时,不能够把内存屏障后面的指令重排序到内存屏障前面执行,而且会及时的将线程工做内存中的数据及时更新到主内存中,进而使得其余的线程可以观察到最新的数据


参考资料

  1. 《深刻理解Java虚拟机》
相关文章
相关标签/搜索