【并发编程笔记】 ---- DCL单例模式及volatile问题

文章目录

1. 单例模式中的懒汉式
2. 单例模式中的双检索/双重校验锁(DCL)
3. 对象创建过程
4. 基于volatile解决方案

1. 单例模式中的懒汉式

1.1 线程不安全的懒汉式

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
  
    public static Singleton getInstance() {  
	    if (instance == null) {  
	        instance = new Singleton();  
	    }  
	    return instance;  
    }  
}
  • 是否是多线程安全:否
  • 实现难度: 易
  • 这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
    这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作

1.2 线程安全的懒汉式

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
    public static synchronized Singleton getInstance() {  
	    if (instance == null) {  
	        instance = new Singleton();  
	    }  
    	return instance;  
    }  
}
  • 是否是多线程安全: 是
  • 实现难度: 易
  • 这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
    优点:第一次调用才初始化,避免内存浪费。
    缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
    getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。

2. 单例模式中的双检索/双重校验锁(DCL)

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
	    if (singleton == null) {  
	        synchronized (Singleton.class) {  
		        if (singleton == null) {  
		            singleton = new Singleton();  
		        }  
	        }  
	    }  
   	    return singleton;  
    }  
}
  • JDK 版本:JDK1.5 起
  • 是否 Lazy 初始化:是
  • 是否多线程安全:是
  • 实现难度:较复杂
    描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
    getInstance() 的性能对应用程序很关键。

3. 对象创建过程

3.1 CPU指令层面可能分为三个步骤:(如下伪代码)

memory = allocate(); //1分配对象内存空间
ctorInstance(memory); // 2初始化对象
instance = memory; //3设置instance指向更分配的内存地址

这几个步骤的执行时序在单线程情况下可能如下:

多线程情况下:

3.2 或者查看字节码层面上的对象初始化过程

public class JustTest {
	public static void main(String[] args) {
		Object o = new Object();
	}
}

这里需要安装插件查看字节码:在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
此时 如果发生指令重排序
在这里插入图片描述
线程1半初始化的时候,线程2此时访问,由于半初始化的时候对象不为Null,所以线程2会直接拿已经半初始化的对象,而没有拿到完全初始化的对象。

4. 基于volatile解决方案

4.1 将变量singleton声明为volatile即可

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后,步骤 2、3 就不会被重排序了

4.2 volatile如何解决指令重排序(实现细节)

  1. 编译器层面
    在这里插入图片描述

  2. JVM层面
    在这里插入图片描述
    JSR内存屏障
    在这里插入图片描述

  3. hotspot实现

    bytecodeinterpreter.cpp

    int field_offset = cache->f2_as_index();
              if (cache->is_volatile()) {
                if (support_IRIW_for_not_multiple_copy_atomic_cpu) {
                  OrderAccess::fence();
                }
    

    orderaccess_linux_x86.inline.hpp

    inline void OrderAccess::fence() {
      if (os::is_MP()) {
        // always use locked addl since mfence is sometimes expensive
    	#ifdef AMD64
    	    __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
    	#else
    	    __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
    	#endif
    	}
    }
    

    “lock; addl $0,0(%%esp)” 相当于一个内存屏障(Memory Barrier, 指重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障,但如果有两个或者更多CPU访问同一块内存,且其中有一个在观测另一个,就需要内存屏障来保证一致性。addl $0,0(%%esp)是一条空操作,可以让前面volatile变量的修改对其它CPU立即可见

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值