前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >java并发编程实战(5) happen-before原则和彻底明白DCL的volatile作用。

java并发编程实战(5) happen-before原则和彻底明白DCL的volatile作用。

作者头像
黄规速
发布2022-04-14 15:23:54
3450
发布2022-04-14 15:23:54
举报
文章被收录于专栏:架构师成长之路

应用场景:我们需要一个单例模式:一个类有且仅有一个实例,并且自行实例化向整个系统提供。

一、普通懒汉式单例模式

我们先看单例模式里面的懒汉式:

public class Singleton { private static Singleton singleton;

private Singleton(){}

public static Singleton getInstance(){ if(singleton == null){ singleton = new Singleton(); }

return singleton; } } 我们都知道这种写法是错误的,因为它无法保证线程的安全性。为了确保安全性,就是在getInstance方法上面做了同步,但是synchronized就会导致这个方法比较低效,导致程序性能下降。

因此我们把synchronized放在方法同步为:DCL 即Double Check Lock,中卫双重检查锁定。

代码语言:javascript
复制
public class Singleton {
    private static Singleton singleton;

    private Singleton(){}

    public static Singleton getInstance(){
        if(singleton == null){                              // 1
            synchronized (Singleton.class){                 // 2
                if(singleton == null){                      // 3
                    singleton = new Singleton();            // 4
                }
            }
        }
        return singleton;
    }
}

就如上面所示,DCL:A 线程进行判空检查之后开始执行synchronized代码块时发生线程切换(线程切换可能发生在任何时候),B 线程也进行判空检查,B线程检查 singleton == null 结果为true,也开始执行synchronized代码块,虽然synchronized 会让二个线程串行执行,如果synchronized代码块内部不进行二次判空检查,singleton 可能会初始化二次。

这个代码看起来很完美,理由如下:

  1. 如果检查第一个singleton不为null,则不需要执行下面的加锁动作,极大提高了程序的性能;
  2. 如果第一个singleton为null,即使有多个线程同一时间判断,但是由于synchronized的存在,只会有一个线程能够创建对象;
  3. 当第一个获取锁的线程创建完成后singleton对象后,其他的在第二次判断singleton一定不会为null,则直接返回已经创建好的singleton对象;

但是这种DCL方式其实是有缺陷的,具体什么缺陷呢?我们先看看happen-before原则

二、happen-before原则

Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程的程序执行,却会影响到多线程并发执行的正确性。可以通过volatile、synchronize、Lock来保证一定的有序性。

JMM中具有一定先天的有序性,不需要任何手段就能保证有序性,这就是我们经常见到的happens-before原则。重排序需要遵守happens-before规则,不能说你想怎么排就怎么排。

Happen-Before八大原则:

1)单线程happen-before原则(程序顺序规则):在同一个线程中,书写在前面的操作happen-before后面的操作。 这条规则是说,在单线程中操作间happen-before关系完全是由源代码的顺序决定的,这里的前提“在同一个线程中”是很重要的。 2)锁的happen-before原则(锁管理原则):同一个锁的unlock操作happen-before此锁的lock操作。 这里的“后续”指的是时间上的先后关系,unlock操作发生在退出同步块之后,lock操作发生在进入同步块之前。这是条最关键性的规则,线程安全性主要依赖于这条规则。 这里关键条件是必须对“同一个锁”的lock和unlock。但是仅仅是这条规则仍然不起任何作用,它必须和传递性原则联合起来使用才显得意义重大。 3)volatile的happen-before原则():对一个volatile变量的写操作happen-before对此变量的任意(读/写)操作(当然也包括写操作了)。 4)happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。

前四条规则是比较重要的,后四条是比较显而易见的。

5)线程启动的happen-before原则(start()规则):同一个线程的start方法happen-before此线程的其它方法。 6)线程中断的happen-before原则(程序中断规则):即对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。(线程t1写入的所有变量,调用Thread.interrupt(),被打断的线程t2,可以看到t1的全部操作) 7)线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。 (线程t1写入的所有变量,在任意其它线程t2调用t1.join(),或者t1.isAlive() 成功返回后,都对t2可见。) 8)对象创建的happen-before原则(对象finalize规则):一个对象的初始化完成先于他的finalize方法调用。(对象调用finalize()方法时,对象初始化完成的任意操作,同步到全部主存同步到全部cache。)

如果两个操作的执行次序无法从happens-before规则推导出来,那么就不能保证他们的有序性,虚拟机就可以随意的对他们进行重排序。

举例(即happens-before关系并不代表了最终的执行顺序)

代码语言:javascript
复制
public double rectangleArea(double length , double width){
        double len;
        double wid;
        len=length;//A
        wid=width;//B
        double area=leng*wid;//C
        return area;
}

上面的操作在运行之前编译器和处理器可能会进行优化,在程序中

A happens-before B

B happens-before C

A happens-before C //happens-before具有传递规则

(1)因为A happens-before B,所以A操作产生的结果leng一定要对B操作可见,但是现在B操作并没有用到length,所以这两个操作可以重排序;

(2)如果A操作和C操作进行了重排序,因为leng没有被赋值,所以leng=0,area=0*wid也就是area=0,A和C不能进行重排序;

三、DCL方式的缺陷分析

但是这种DCL方式其实是有缺陷的,具体什么缺陷呢?

当线程A在获取了Instance.class锁时,对singleton进行 singleton = new Instance() 初始化时,实例化一个对象要分为三个步骤:

step1、 分配内存空间。 step2、 初始化对象。 step3、 将内存空间的地址赋值给对应的引用。

我们使用synchronized ,在synchronized代码块内 ,指令符合锁的happen-before原则(锁管理原则),但step2和step3这两步并不违反Happen-Before规则,因此可以指令重排序,类似的有x=1,y=1,z=x+y可以被编译器优化为y=1,x=1,z=x+y。 正是因为指令重排序,所以上面的过程也可能变为如下的过程: step1、 分配内存空间。 step3、 将内存空间的地址赋值给对应的引用。 step2、 初始化对象 。

如果发生了重排序:,此时线程A的singleton只分配内存地址,但还没有初始化对象,此时singleton是不为空的。

那么就回导致线程B在红框部分判断会出错,直接返回singleton变量,即return的singleton对象是一个没有被初始化的对象。

当然,指令重排序的问题并非每次都会进行,在某些特殊的场景下,编译器和处理器是不会进行重排序的,但上述的举例场景则是大概率会出现指令重排序问题。

四、解决方案

1、基于volatile解决方案

java并发编程实战(2):volatile实现原理:volatile的特性是确保变量可见性和禁止指令重排。

对于上面的DCL其实只需要做一点点修改即可:将变量singleton声明为volatile即可:

代码语言:javascript
复制
public class Singleton {
    //通过volatile关键字来确保安全
    private volatile static Singleton singleton;

    private Singleton(){}

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

当singleton声明为volatile后:对它的写操作就会有一个内存屏障,这样,在singleton的赋值完成之前,就不用会调用读操作。

注意:volatile阻止的不是singleton = newSingleton()这句话内部[step1、step2、step3]的指令重排,而是保证了在一个写操作([step1、step2、step3]完成之前,不会调用读操作(if (instance == null))。

2、基于饿汉模式的类初始化解决方案

该解决方案的根本就在于:利用classloder的机制来保证初始化singleton时只有一个线程。JVM在类初始化阶段会获取一个锁,这个锁可以同步多个线程对同一个类的初始化

代码语言:javascript
复制
public class Singleton {
    private static class SingletonHolder{
        public static Singleton singleton = new Singleton();
    }

    public static Singleton getInstance(){
        return SingletonHolder.singleton;
    }
}

这种解决方案的实质是:运行步骤2和步骤3重排序,但是不允许其他线程看见。缺点就是有可能singleton在整个程序运行生命周期都没有用到。

3、内部类解决

代码语言:javascript
复制
public class Singleton {

    /**
     * 构造方法私有化
     */
    private Singleton(){
    }
    
    private static class SingleHolder{
        private static final Singleton singleton = new Singleton();
    }

    /**
     * 内部类方式获取单例
     * @return
     */
    public static Instance getInstance(){
        return SingleHolder.singleton;
    }
    
}

这种从jvm虚拟机上保证了单例,并且也是懒式加载。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018/01/25 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、普通懒汉式单例模式
  • 二、happen-before原则
  • 三、DCL方式的缺陷分析
  • 四、解决方案
    • 1、基于volatile解决方案
      • 2、基于饿汉模式的类初始化解决方案
    • 3、内部类解决
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档