我们第一次写的单例模式是下面这样的:

 public class Singleton {
private static Singleton instance = null;
public static Singleton getInstance() {
if(null == instance) { // line A
instance = new Singleton(); // line B
} return instance; }
}

  假设这样的场景:两个线程并发调用Singleton.getInstance(),假设线程一先判断instance是否为null,即代码中line A进入到line B的位置。刚刚判断完毕后,JVM将CPU资源切换给线程二,由于线程一还没执行line B,所以instance仍然为空,因此线程二执行了new Singleton()操作。片刻之后,线程一被重新唤醒,它执行的仍然是new Singleton()操作,这样问题就来了,new出了两个instance,这还能叫单例吗?

  紧接着,我们再做单例模式的第二次尝试:

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

  比起第一段代码仅仅在方法中多了一个synchronized修饰符,现在可以保证不会出线程问题了。但是这里有个很大(至少耗时比例上很大)的性能问题。除了第一次调用时是执行了Singleton的构造函数之外,以后的每一次调用都是直接返回instance对象。返回对象这个操作耗时是很小的,绝大部分的耗时都用在synchronized修饰符的同步准备上,因此从性能上来说很不划算

  继续把代码改成下面这样:

  

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

  基本上,把synchronized移动到代码内部是没有什么意义的,每次调用getInstance()还是要进行同步。同步本身没有问题,但是我们只希望在第一次创建instance实例的时候进行同步,因此有了下面的写法——双重锁定检查(DCL,Double Check Lock)

 public class Singleton {
private static Singleton instance = null;
public static Singleton getInstance() {
if(null == instance) { // 线程二检测到instance不为空
synchronized (Singleton.class) {
if(null == instance) {
instance = new Singleton(); // 线程一被指令重排,先执行了赋值,但还没执行完构造函数(即未完成初始化)
}
}
} return instance; // 后面线程二执行时将引发:对象尚未初始化错误 }
}

  看样子已经达到了要求,除了第一次创建对象之外,其它的访问在第一个if中就返回了,因此不会走到同步块中,已经完美了吗?

  如上代码段中的注释:假设线程一执行到instance = new Singleton()这句,这里看起来是一句话,但实际上其被编译后在JVM执行的对应会变代码就发现,这句话被编译成8条汇编指令,大致做了三件事情:

  1)给instance实例分配内存;

  2)初始化instance的构造器;

  3)将instance对象指向分配的内存空间(注意到这步时instance就非null了)

  如果指令按照顺序执行倒也无妨,但JVM为了优化指令,提高程序运行效率,允许指令重排序。如此,在程序真正运行时以上指令执行顺序可能是这样的:

  a)给instance实例分配内存;

  b)将instance对象指向分配的内存空间;

  c)初始化instance的构造器;

  这时候,当线程一执行b)完毕,在执行c)之前,被切换到线程二上,这时候instance判断为非空,此时线程二直接来到return instance语句,拿走instance然后使用,接着就顺理成章地报错(对象尚未初始化)。

  具体来说就是synchronized虽然保证了线程的原子性(即synchronized块中的语句要么全部执行,要么一条也不执行),但单条语句编译后形成的指令并不是一个原子操作(即可能该条语句的部分指令未得到执行,就被切换到另一个线程了)

  根据以上分析可知,解决这个问题的方法是:禁止指令重排序优化,即使用volatile变量

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

  将变量instance使用volatile修饰即可实现单例模式的线程安全。

  关于volatile的用法在此不展开,之后会另行介绍。

DCL单例模式的更多相关文章

  1. 从一个小例子引发的Java内存可见性的简单思考和猜想以及DCL单例模式中的volatile的核心作用

    环境 OS Win10 CPU 4核8线程 IDE IntelliJ IDEA 2019.3 JDK 1.8 -server模式 场景 最初的代码 一个线程A根据flag的值执行死循环,另一个线程B只 ...

  2. DCL单例模式中的缺陷及单例模式的其他实现

    DCL:Double Check Lock ,意为双重检查锁.在单例模式中懒汉式中可以使用DCL来保证程序执行的效率. 1 public class SingletonDemo { 2 private ...

  3. Volatile的应用DCL单例模式(四)

    Volatile的应用 单例模式DCL代码 首先回顾一下,单线程下的单例模式代码 /** * 单例模式 * * @author xiaocheng * @date 2020/4/22 9:19 */ ...

  4. Volatile关键字&&DCL单例模式,volatile 和 synchronized 的区别

    Volatile 英文翻译:易变的.可变的.不稳定的. 一.volatile 定义及用法 多个线程的工作内存彼此独立,互不可见,线程启动的时候,虚拟机为每个内存分配一块工作内存,不仅包含了线程内部定义 ...

  5. 单例模式(Singleton)看了就懂

    单例,故名思议,一个只能创建一个实例的类. 单例被广泛应用于Spring的bean(默认).线程池.数据库连接池.缓存,还有其他一些无状态的类如servlet. 一个没必要多例的类实现了单例可以节约空 ...

  6. Android设计模式之单例模式的七种写法

    一 单例模式介绍及它的使用场景 单例模式是应用最广的模式,也是我最先知道的一种设计模式.在深入了解单例模式之前.每当遇到如:getInstance()这样的创建实例的代码时,我都会把它当做一种单例模式 ...

  7. 单例DCL模式

    单例模式可以保证系统中一个类只有一个实例.即一个类只有一个对象实例. 一般写法 public class DCLSingle { public static DCLSingle instance= n ...

  8. GoF23:单例模式(singleton)

    目录 单例模式简介 常见五种单例模式的实现方式 饿汉式 懒汉式 DCL懒汉式 饿汉式改进(静态内部类式) 枚举单例 防止反射破坏单例模式 单例模式简介 核心作用:保证一个类只有一个实例,并且提供一个访 ...

  9. 【设计模式】单例设计模式的N中Java实现方法

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/17359719 特点 单例模式的特点: 1.只能有一个实例: 2.必须自己创建自己的一个实例 ...

随机推荐

  1. vim 快捷键绑定

    1. 退出VIM,在shell下编译&&运行程序 :wq [root@fsjohnhuang ~]# gcc main.c -o main [root@fsjohnhuang ~]# ...

  2. docker 系列 - 容器数据持久化和数据共享

    docker 主要有两种数据存储形式, 一种是storage driver(也叫做 Graph driver), 另一种是 volume driver. stroage driver主要是存储那些无状 ...

  3. Linux下的解压命令

    Linux下常见的压缩包格式有5种:zip tar.gz tar.bz2 tar.xz tar.Z 其中tar是种打包格式,gz和bz2等后缀才是指代压缩方式:gzip和bzip2 filename. ...

  4. 4.mysql(slow_log/generic_log/audit)

    一.慢查询日志进阶 <1>相关参数 1.slow_query_log #是否开启慢查询日志 2.slow_query_log_file #慢查询日志文件名,在my.cnf我们已经定义为sl ...

  5. lua 立即执行函数

    背景 不同文件中,lua提供模块写法, 使用local修饰,可以将变量或者函数,声明为模块内有效,例如 模块暴漏变量使用 return 表的方式. local aafunc = function() ...

  6. 从零开始搭建轻量级个人XSS平台

    一. 前言 决定搭建XSS平台是因为自己想深入学习一下XSS相关的知识,多多进行实践,上网搜索了一下XSS平台有很多,但是总觉得不是很安全,这个毕竟敏感信息要传输到陌生人的服务器上,而且服务器端测试代 ...

  7. httpd.conf文件与.htaccess文件的对比

    httpd.conf文件与.htaccess文件相比,Apache对两者的mod_rewrite规则在处理方法上有些细微的差别.在实 际运行时,如果有任何原因使得倾向于使用httpd.conf,都需要 ...

  8. C++ 类2

    继承: 联系一起的类有层次关系. 基类: 层次关系的根部 派生类: 基类负责定义在层次关系中所以类共同拥有的成员,派生类定义其各种特有的成员. 基类类型相关的函数, 派生类不做改变直接继承的函数 某些 ...

  9. pyqtgraph

    安装  pip3 install pyqtgraph 在PyQtGraph中,有几种绘制图形的方法: pyqtgraph.plot():创建一个新的绘图窗口来显示数据: PlotWidget.plot ...

  10. 结构体类型struct

    教学视频 定义: struct student{CString name; int num; TCHAR sex; int age; };   //注意有个分号 student zansan = {_ ...