DCL,即Double Check Lock,中卫双重检查锁定。其实DCL很多人在单例模式中用过,LZ面试人的时候也要他们写过,但是有很多人都会写错。他们为什么会写错呢?其错误根源在哪里?有什么解决方案?下面就随LZ一起来分析

问题分析

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

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

我们都知道这种写法是错误的,因为它无法保证线程的安全性。优化如下:

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

优化非常简单,就是在getInstance方法上面做了同步,但是synchronized就会导致这个方法比较低效,导致程序性能下降,那么怎么解决呢?聪明的人们想到了双重检查 DCL:

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;
}
}

就如上面所示,这个代码看起来很完美,理由如下:

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

通过上面的分析,DCL看起确实是非常完美,但是可以明确地告诉你,这个错误的。上面的逻辑确实是没有问题,分析也对,但是就是有问题,那么问题出在哪里呢?在回答这个问题之前,我们先来复习一下创建对象过程,实例化一个对象要分为三个步骤:

  1. 分配内存空间
  2. 初始化对象
  3. 将内存空间的地址赋值给对应的引用

但是由于重排序的缘故,步骤2、3可能会发生重排序,其过程如下:

  1. 分配内存空间
  2. 将内存空间的地址赋值给对应的引用
  3. 初始化对象

如果2、3发生了重排序就会导致第二个判断会出错,singleton != null,但是它其实仅仅只是一个地址而已,此时对象还没有被初始化,所以return的singleton对象是一个没有被初始化的对象,如下:

按照上面图例所示,线程B访问的是一个没有被初始化的singleton对象。

通过上面的阐述,我们可以判断DCL的错误根源在于步骤4:

singleton = new Singleton();

知道问题根源所在,那么怎么解决呢?有两个解决办法:

  1. 不允许初始化阶段步骤2 、3发生重排序。
  2. 允许初始化阶段步骤2 、3发生重排序,但是不允许其他线程“看到”这个重排序。

解决方案

解决方案依据上面两个解决办法即可。

基于volatile解决方案

对于上面的DCL其实只需要做一点点修改即可:将变量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就不会被重排序了,也就可以解决上面那问题了。

基于类初始化的解决方案

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

public class Singleton {
private static class SingletonHolder{
public static Singleton singleton = new Singleton();
} public static Singleton getInstance(){
return SingletonHolder.singleton;
}
}

这种解决方案的实质是:运行步骤2和步骤3重排序,但是不允许其他线程看见。

Java语言规定,对于每一个类或者接口C,都有一个唯一的初始化锁LC与之相对应。从C到LC的映射,由JVM的具体实现去自由实现。JVM在类初始化阶段期间会获取这个初始化锁,并且每一个线程至少获取一次锁来确保这个类已经被初始化过了。

参考资料

  1. 方腾飞:《Java并发编程的艺术》

Java内存模型之从JMM角度分析DCL的更多相关文章

  1. 全面理解Java内存模型(JMM)及volatile关键字(转载)

    关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoad ...

  2. 全面理解Java内存模型(JMM)及volatile关键字

    [版权申明]未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) http://blog.csdn.net/javazejian/article/details/72772461 出自[zejian ...

  3. 全面理解Java内存模型(JMM)

    理解Java内存区域与Java内存模型Java内存区域 Java虚拟机在运行程序时会把其自动管理的内存划分为以上几个区域,每个区域都有的用途以及创建销毁的时机,其中蓝色部分代表的是所有线程共享的数据区 ...

  4. 全面理解Java内存模型(JMM)及volatile关键字(转)

    原文地址:全面理解Java内存模型(JMM)及volatile关键字 关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型( ...

  5. Java并发编程(1)-Java内存模型

    本文主要是学习Java内存模型的笔记以及加上自己的一些案例分享,如有错误之处请指出. 一 Java内存模型的基础 1.并发编程模型的两个问题 在并发编程中,需要了解并会处理这两个关键问题: 1.1.线 ...

  6. 深入理解java内存模型

    深入理解Java内存模型(一)——基础 深入理解Java内存模型(二)——重排序 深入理解Java内存模型(三)——顺序一致性 深入理解Java内存模型(四)——volatile 深入理解Java内存 ...

  7. 最简单的 Java内存模型 讲解

    前言 在网上看了很多文章,也看了好几本书中关于JMM的介绍,我发现JMM确实是Java中比较难以理解的概念.网上很多文章中关于JMM的介绍要么是照搬了一些书上的内容,要么就干脆介绍的就是错的.本文试着 ...

  8. jvm系列五-java内存模型(2)

    原作者系列文章链接:并发编程系列博客传送门 前言# 在网上看了很多文章,也看了好几本书中关于JMM的介绍,我发现JMM确实是Java中比较难以理解的概念.网上很多文章中关于JMM的介绍要么是照搬了一些 ...

  9. Java并发编程(四)-- Java内存模型

    Java内存模型 前面讲到了Java线程之间的通信采用的是共享内存模型,这里提到的共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入何时对另一个线程可见.从抽象的角 ...

随机推荐

  1. windows下安装anaconda和tensorflow

    anaconda确实很好用,省去了很多麻烦,现在我个人推荐直接使用anaconda. anaconda的特点:可以存在多个python环境,要使用某一个环境的话,就需要切换到这个环境,安装.卸载包都是 ...

  2. Qt编写自定义控件62-探探雷达

    一.前言 随着移动互联网的盛行,现在手机APP大行其道,每个人的手机没有十几个APP都不好意思说自己是现代人,各种聊天.购物.直播.小视频等APP,有个陌生人社交的APP叫探探,本人用过几次,当然不是 ...

  3. Zabbix 3.2.6-Mysql多实例监控-Percona Monitoring Plugins自动发现

    mysql多实例监控实录   系统环境: cat /etc/redhat-release CentOS Linux release 7.3.1611 (Core) 内核版本: uname -r 3.1 ...

  4. LeetCode_383. Ransom Note

    383. Ransom Note Easy Given an arbitrary ransom note string and another string containing letters fr ...

  5. yaml中使用harbor

    1.在harbor的ui界面上注册一个账号 姓名:zihao 全名:zhuzihao 密码:Zihao@5tgb 邮箱:15613691030@163.com 2.在需要下载镜像的机器上,同样需要修改 ...

  6. unsupported media type 415

    jquery  ajax请求报错解决方案: 两点: data: JSON.stringify(obj), contentType:"application/json", 完整pos ...

  7. Js获取file上传控件的文件路径总结

    总结一个获取file上传控件文件路径的方法 firefox由于保护机制只有文件名,不能获取完整路径. document.getElementById('file').onchange = functi ...

  8. app--Android 稳定性测试方案

    标准的崩溃日志:Android 应用一般使用 Java 语言开发,在不作特殊处理的情况下,遇到未处理的异常时,会弹框提示“程序遇到异常,即将退出”(有些手机框中提示语不同, 或不弹框).同时会将异常的 ...

  9. QT QML之Label, TextField

    现在不是去想缺少什么的时候,该想一想凭现有的东西你能做什么.------ 海明威 <老人与海> Label { id: tipLabel width: 120 height: 40 tex ...

  10. urls 视图层

    urls.py 路由层 路由与视图函数对应关系 >>> 路由层 URL配置(URLconf)就像Django 所支撑网站的目录.它的本质是URL与要为该URL调用的视图函数之间的映射 ...