【java并发编程】DCL单例模式与Happen-Before

本来想写一篇文章说说DCL的缺陷顺带说一下JMM,看到有一篇文章写的不错,就直接转过来修改了一下。原文出处在这里

1 前言

单例模式是我们经常使用的一种模式,一般来说很多资料都建议我们写成如下的模式:

public class Instance {
    private String str = "";
    private int a = 0;

    private static Instance ins = null;
    /**
     * 构造方法私有化
     */
    private Instance(){
        str = "hello";
        a = 20;
    }

    /**
     * DCL方式获取单例
     * @return
     */
    public static Instance getInstance(){
        if (ins == null){
            synchronized (Instance.class){
                ins = new Instance();
            }
        }
        return ins;
    }
}

但是这种方式其实是有缺陷的,具体什么缺陷呢?我们首先要了解JVM了内存模型,请看下面分析

2 JVM内存模型

JVM模型如下图:

这里着重介绍下VM Stack,其他的我相信都比较熟悉。
VM Stack是线程私有的区域。他是java方法执行时的字典:它里面记录了局部变量表、 操作数栈、 动态链接、 方法出口等信息。

在《java虚拟机规范》一书中对这部分的描述如下:

栈帧是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接、方法返回值和异常分派。

栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。
栈帧的存储空间分配在 Java 虚拟机栈之中,每一个栈帧都有自己的局部变量表、操作数栈和指向当前方法所属的类的运行时常量池的引用。

java中某个线程在访问堆中的线程共享变量时,为了加快访问速度,提升效率,会把该变量临时拷贝一份到自己的VM Stack中,并保持和堆中数据的同步。

3 传统DCL方式的缺陷

有了以上的基础知识我们就可以知道DCL方式的缺陷在哪儿了。

当线程A在获取了Instance.class锁时,对ins进行 ins = new Instance() 初始化时,由于这是很多条指令,jvm可能会乱序执行。这个时候如果线程B在执行if (ins == null)时,正常情况下,如果为true,说明需要获取Instance.class锁,等待初始化。但是这时候,假设线程A再没有对ins进行初始化完,比如只对str进行了赋值,还没有来的及对a进行赋值,假如jvm将未完成赋值的值拷贝回堆中,这个时候线程B有可能读到的值就不是为null了,就会造成数据丢失的情况。这时候我们发现线程B获取的对象中a的值是0,而不是20。

总结起来:对ins的写操作不 happen-before 对它的读操作

这就是DCL方式的缺陷,那么怎么避免呢?

4 happen-before原则

JMM为程序中所有的操作定义了一个偏序关系,成为Happen-Before。要想保证执行操作B的线程看到线程A的结果,那么A和B之间必须满足Happen-Before规则

当一个变量被多个线程读取并且至少被一个线程写入时,如果在读操作和写操作之间没有依照Happen-Before来排序,那么就会产生数据竞争的问题。

具体来说,happen-before规则包括:

  1. 程序顺序规则:如果程序中操作A在操作B之前,那么B操作可以看到A操作的所有内存改变。
  2. 监视器锁规则:如果线程1解锁了monitor A,接着线程2锁定了A,那么,线程1解锁A之前的写操作都对线程2可见(线程1和线程2可以是同一个线程)。
  3. volatile变量规则:如果线程1写入了volatile变量V,接着线程2读取了V,那么,线程1写入V及之前的写操作都对线程2可见(线程1和线程2可以是同一个线程)
  4. 线程启动规则:在线程上对Thread.start的调用必须在该线程中执行任何操作之前执行。
  5. 线程结束规则:线程中的任何操作都必须在其他线程检测到线程A已经结束(或者从Thread.join中成功返回,或者在调用Thread.isAlive时返回false)之前执行完成。
  6. 中断规则:当一个线程在另一个线程上调用interrupt时,必须在被中断线程检测到interrupt调用之前执行(通过抛出InterruptedException,或者调用isInterrupted和interrupted)。
  7. 终结器规则:对象的构造函数必须在启动该对象的终结器之前执行完成。
  8. 传递性:如果操作A在操作B之前执行,并且操作B在操作C之前执行,那么操作A必须在操作C之前执行。

4 单例模式的正确写法

有了以上的分析我们知道,我们只需要在保证对ins的访问是读在写之后即可,因此正确的做法是在ins 前加上一个关键字volatile。因此DCL的正确写法应该如下:

public class Instance {
    private String str = "";
    private int a = 0;

    private volatile static Instance ins = null;
    /**
     * 构造方法私有化
     */
    private Instance(){
        str = "hello";
        a = 20;
    }

    /**
     * DCL方式获取单例
     * @return
     */
    public static Instance getInstance(){
        if (ins == null){
            synchronized (Instance.class){
                ins = new Instance();
            }
        }
        return ins;
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值