所谓的DCL 就是 Double Check Lock,即双重锁定检查,在了解DCL在单例模式中如何应用之前,我们先了解一下单例模式。单例模式通常分为“饿汉”和“懒汉”,先从简单入手

饿汉

所谓的“饿汉”是因为程序刚启动时就创建了实例,通俗点说就是刚上菜,大家还没有开始吃的时候就先自己吃一口。

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

第3行 通过一个私有构造方法限制了创建此类对象的途径(反射忽略)。这种方法很安全,但从某种程度上有点浪费资源,比方说从一开始就创建了Singleton实例,但很少去用它,这就造成了方法区资源的浪费,因此出现了另外一种单例模式,即懒汉单例模式

懒汉

之所以叫“懒汉”是因为只有真正叫它的时候,才会出现,不叫它它就不理,跟它没关系。也就是说真正用到它的时候才去创建实例,并不是一开始就创建实例。如下代码所示:


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

看似很简单的一段代码,但存在一个问题,就是线程不安全的问题。例如,现在有1000个线程,都需要这一个Singleton的实例,验证一下是否拿到同一个实例,代码如下所示:

public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance(){
if(null == singleton){
try {
Thread.sleep(1);//象征性的睡了1ms
} catch (InterruptedException e) {
e.printStackTrace();
}
singleton = new Singleton();
}
return singleton;
} public static void main(String[] args) {
for (int i=0;i<1000;i++){
new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();
}
}
}

部分运行结果,乱七八糟:

944436457
1638599176
710946821
67862359

为什么会这样?第一个线程过来了,执行到第7行,睡了1ms,正在睡的同时第二个线程来了,第二个线程执行到第5行时,结果肯定为空,因此接下来将会有两个线程各自创建一个对象,这必然会导致Singleton.getInstance().hashCode()结果不一致。可以通过给整个方法加上一把锁改进如下:

改进1

public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(null == singleton){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
singleton = new Singleton();
}
return singleton;
} public static void main(String[] args) {
for (int i=0;i<1000;i++){
new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();
}
}
}

通过给getInstance()方法加上synchronized来解决线程一致性问题,结果分析虽然显示所有实例的hashcode都一致,但是synchronized的粒度太大了,即锁的临界区太大了,有点影响效率,例如如果第4行和第5行之间有业务处理逻辑,不会涉及共享变量,那么每次对这部分业务逻辑加锁必然会导致效率低下。为了解决粗粒度的问题,可以对代码进一步改进:

改进2

public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance(){
/*
一堆业务处理代码
*/
if(null == singleton){
synchronized(Singleton.class){//锁粒度变小
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
singleton = new Singleton();
}
}
return singleton;
} public static void main(String[] args) {
for (int i=0;i<1000;i++){
new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();
}
}
}

部分运行结果 :

391918859
391918859
391918859
1945023194

通过分析运行结果发现,虽然锁的粒度变小了,但线程不安全了。为什么会这样呢?因为有种情况,线程1执行完if判断后还没有拿到锁的时候时间片用完了,此时线程2来了,执行if判断时发现对象还是空的,继续往下执行,很顺利的拿到锁了,因此线程2创建了一个对象,当线程2创建完之后释放掉锁,这时线程1激活了,顺利的拿到锁,又创建了一个对象。所以代码还需要再一步的改进。

改进3

public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance(){
/*
一堆业务处理代码
*/
if(null == singleton){
synchronized(Singleton.class){//锁粒度变小
if(null == singleton){//DCL
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
singleton = new Singleton();
}
}
}
return singleton;
} public static void main(String[] args) {
for (int i=0;i<1000;i++){
new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();
}
}
}

通过在第10行又加了一层if判断,也就是所谓的Double Check Lock。也就是说即便拿到锁了,也得去作一步判断,如果这时判断对像不为空,那么就不用再创建对象,直接返回就可以了,很好的解决了“改进2”中的问题。但这里第8行是不是可以去了,我个人觉得都行,保留第8行的话,是为了提升效率,因为如果去了,每个线程过来就直接抢锁,抢锁本身就会影响效率,而if判断就几ns,且大部分线程是不需要抢锁的,所以最好保留。
到这DCL 单例的原理就介绍完了,但是还是有一个问题。就是需要考虑指令重排序的问题,因此得加入volatile来禁止指令重排序。继续分析代码,为了分析方便这里将Singleton代码简化:

public class Singleton {
int a = 5;//考虑指令重排序的问题
}

singleton = new Singleton()的字节码如下:

  0: new    #2           // class com/reasearch/Singleton
3: dup
4: invokespecial #3 // Method com/reasearch/Singleton."<init>":()V
7: astore_1

先不管dup指令。这里补充一个知识点,创建对象的时候,先分配空间,类里面的变量先有一个默认值,等调用了构造方法后才给变量赋值。例如int a = 5刚开始的时候 a = 0。字节码指令执行过程如下,

  1. new 分配空间,a=0
  2. invokespecial 构造方法 a=5
  3. astore_1将对象赋给singleton

这是理想的状态,2和3语义和逻辑上没有什么关联,因此jvm可以允许这些指令乱序执行,即先执行3再执行2 。回到改进3,假如线程1再执行第16行代码时,指令的执行顺序是1,3,2,当执行完3时,时间片用完了,此时a=0,也就是说初始化到一半时就挂起了。这时线程2 来了,第8行判断,singleton肯定不为空,因此直接返回一个Singleton的对象,但其实这个对象是一个问题对象,是一个半初始化的对象,即a=0。这就是指令重排序造成的,因此为了防止这种现象的发生加上关键字volatile就可以了。因而,最终DCL之单例模式的代码完整版如下:

完整版

public class Singleton {
private volatile static Singleton singleton = null;//加上volatile
private Singleton(){}
public static Singleton getInstance(){
/*
一堆业务处理代码
*/
if(null == singleton){
synchronized(Singleton.class){//锁粒度变小
if(null == singleton){//DCL
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
singleton = new Singleton();
}
}
}
return singleton;
}
}

至此,可以告一段落了,相信很多小伙伴都会写单例,但是了解其中的原理还是有一定的难度,大家一起加油!

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

  1. DCL并非单例模式专用

    我相信大家都很熟悉DCL,对于缺少实践经验的程序开发人员来说,DCL的学习基本限制在单例模式,但我发现在高并发场景中会经常遇到需要用到DCL的场景,但并非用做单例模式,其实DCL的核心思想和CopyO ...

  2. Android设计模式——单例模式

    1.单例模式就是确保一个类,只有一个实例化对象,而且自行实例化并向整个系统提供这个实例. 2.使用场景: 确保某个类,有且只有一个对象,避免产生对个对象,消耗过多的资源. 2.实现单例模式的重要点: ...

  3. java笔记--问题总结

    1. 垃圾回收算法 标记-清除算法 标记-清除算法是最基本的算法,和他的名字一样,分为两个步骤,一个步骤是标记需要回收的对象.在标记完成后统一回收被标记的对象.这个算法两个问题.一个是效率问题,标记和 ...

  4. 悟空模式-java-单例模式

    [那座山,正当顶上,有一块仙石.其石有三丈六尺五寸高,有二丈四尺围圆.三丈六尺五寸高,按周天三百六十五度:二丈四尺围圆,按政历二十四气.上有九窍八孔,按九宫八卦.四面更无树木遮阴,左右倒有芝兰相衬.盖 ...

  5. 深入理解Java中的锁

    转载:https://www.jianshu.com/p/2eb5ad8da4dc Java中的锁 常见的锁有synchronized.volatile.偏向锁.轻量级锁.重量级锁 1.synchro ...

  6. java后端研发经典面试题总结

    垃圾回收算法 1.标记-清除算法 标记-清除算法是最基本的算法,和他的名字一样,分为两个步骤,一个步骤是标记需要回收的对象.在标记完成后统一回收被标记的对象.这个算法两个问题.一个是效率问题,标记和清 ...

  7. 深入理解Java虚拟机(第三版)-13.Java内存模型与线程

    13.Java内存模型与线程 1.Java内存模型 Java 内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到主内存和从内存中取出变量值的底层细节 该变量指的是 实例字 ...

  8. 对象部分初始化:原理以及验证代码(双重检查锁与volatile相关)

    对象部分初始化:原理以及验证代码(双重检查锁与volatile相关) 对象部分初始化被称为 Partially initialized objects / Partially constructed ...

  9. volatile 关键字精讲

    1.错误案例 通过一个案例引出volatile关键字,例如以下代码示例 : 此时没有加volatile关键字两个线程间的通讯就会有问题 public class ThreadsShare { priv ...

随机推荐

  1. Gym - 102062A、B、C、D、E、F、G、H

    比赛链接:https://vjudge.net/contest/409725#problem 题面点此处进入 Gym - 102062A 题意: 就是说比赛一共发a+b+c+d个牌子,现在不带上主人公 ...

  2. 神奇C语言的字串处理库函数

    头文件:#incldue<string.h> 定义:strstr(str1,str2) 函数用于判断字符串str2是否是str1的子串.如果是,则该函数返回str2在str1中首次出现的地 ...

  3. hdu 6814 Tetrahedron 规律+排列组合逆元

    题意: 给你一个n,你需要从1到n(闭区间)中选出来三个数a,b,c(可以a=b=c),用它们构成一个直角四面体的三条棱(可看图),问你从D点到下面的三角形做一条垂线h,问你1/h2的期望 题解: 那 ...

  4. UVALive 7276 Wooden Signs

    详细题目见:http://7xjob4.com1.z0.glb.clouddn.com/0f10204481da21e62f8c145939e5828e 思路:记dp[i][j]表示第i个木板尾部在j ...

  5. Codeforces Round #527 (Div. 3) C. Prefixes and Suffixes (思维,字符串)

    题意:给你某个字符串的\(n-1\)个前缀和\(n-1\)个后缀,保证每个所给的前缀后缀长度从\([1,n-1]\)都有,问你所给的子串是前缀还是后缀. 题解:这题最关键的是那两个长度为\(n-1\) ...

  6. TCP Wrappers原理及简单实验

    1.TCP Wrappers(简易防火墙)简介TCP_Wrappers是一个工作在第四层(传输层)的的安全工具,对有状态连接(TCP)的特定服务进行安全检测并实现访问控制,界定方式是凡是调用libwr ...

  7. python-gen

  8. Docker架构分解

    Docker总架构分解Docker对使用者来讲是一个C/S模式的架构,而Docker的后端是一个非常松耦合的架构,模块各司其职,并有机组合,支撑Docker的运行. 用户是使用Docker Clien ...

  9. MySQL5.6 与 MySQL5.7 的区别

    目录 编译安装区别 初始化的区别 其他区别 编译安装区别 # 5.7在编译安装的时候多了一个 boost 库 [root@db02 mysql-5.7.20]# yum install -y gcc ...

  10. Linux系统诊断必备技能之三:查看信息系统常用命令

    一.概述 Linux操作系统的学习中,CLI下进行操作,需要掌握大量命令,Linux的命令有很多,对于命令的学习大家记住只能是熟能生巧,所以现在把日常使用命令为大家罗列一部分,仅供参考. 二.常用命令 ...