Java的懒汉式双检锁单例模式

文章目录

  • Java的懒汉式双检锁单例模式
    • 一、 实现一个双检锁
    • 二、 为什么线程不安全
    • 三、 关于指令重排序
    • 四、 关于原子操作
    • 五、 实现线程安全的双检锁

首先回忆一下,Java中的单例模式有两种,俗称“饿汉式”和“懒汉式”。

饿汉式

public class SingletonEH {/***是否 Lazy 初始化:否*是否多线程安全:是*实现难度:易*描述:这种方式比较常用,但容易产生垃圾对象。*优点:没有加锁,执行效率会提高。*缺点:类加载时就初始化,浪费内存。*它基于 classloder 机制避免了多线程的同步问题,* 不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,* 在单例模式中大多数都是调用 getInstance 方法,* 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,* 这时候初始化 instance 显然没有达到 lazy loading 的效果。*/private static SingletonEH instance = new SingletonEH();private SingletonEH (){}public static SingletonEH getInstance() {System.out.println("instance:"+instance);System.out.println("加载饿汉式....");return instance;}
}

懒汉式

public class SingletonLHsyn {/***是否 Lazy 初始化:是*是否多线程安全:是*实现难度:易*描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。*优点:第一次调用才初始化,避免内存浪费。*缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。*getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。*/private static SingletonLHsyn instance;private SingletonLHsyn (){}public static synchronized SingletonLHsyn getInstance() {if (instance == null) {instance = new SingletonLHsyn();}return instance;}
}
  • 线程安全:饿汉式在线程还没出现之前就已经实例化了,所以饿汉式一定是线程安全的。懒汉式是非线程安全的,可以通过加同步锁解决线程安全问题。
  • 执行效率:饿汉式没有加任何的锁,因此执行效率比较高。懒汉式一般使用都会加同步锁,效率比饿汉式差。
  • 内存使用:饿汉式在一开始类加载的时候就实例化,无论使用与否,都会实例化,所以会占据空间,浪费内存。懒汉式什么时候用就什么时候实例化,不浪费内存。

懒汉式的线程问题可以通过加同步锁来解决,但是上面的懒汉式的代码的性能有点不好,同步锁加在了方法上面,如果实例之前已经初始化完成,每次调用方法都会去访问到同步锁,性能开销比较大,这个时候实际上不在需要加锁了,直接把实例化好的对象返回就可以了。这就引出了下面要说的双检锁,它其实是对懒汉式的一个优化。

一、 实现一个双检锁

双检锁,顾名思义,两次检查一次锁:

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

外层判空是为了解决已经实例化对象后调用方法的性能问题(访问修饰锁的方法有较大开销),中间加锁的方法是为了保证同一时间只有一个线程去实例化对象,内层判空是为了解决非原子操作可能因指令重排序导致的问题。看似是已经完美了,但是还是不安全,下面举例说明一下。

二、 为什么线程不安全

假设有两个线程,当线程A执行到" instance = new DoubleCheckLock();"这一行,而线程B执行到外层"if (instance == null) "时,可能出现instance还未完成构造,但是此时不为null导致线程B获取到一个不完整的instance。
之所以会出现这种情况,要从JVM的指令重排序说起。

三、 关于指令重排序

指令重排序:是编译器在不改变执行效果的前提下,对指令顺序进行调整,从而提高执行效率的过程。
一个最简单的重排序例子:

int a = 1;
String b = "b";

对于这两行毫无关联的操作指令,编译器可能会将其顺序调整为:

String b = "b";
int a = 1;

此时该操作并不会影响后续指令的执行和执行结果。
再回过头看我们的双检锁内部,对于"instance = new DoubleCheckLock();"这一行代码,它分为三个步骤执行:

  • 1.分配一块内存空间
  • 2.在这块内存上初始化一个DoubleCheckLock的实例
  • 3.将声明的引用instance指向这块内存

第2和第3个步骤都依赖于第1个步骤,但是2和3之间没有依赖关系,那么如果编译器将2和3调换顺序,变成了:

  • 1.分配一块内存空间
  • 2.将声明的引用instance指向这块内存
  • 3.在这块内存上初始化一个DoubleCheckLock的实例

当线程A执行到第2步时,instance已经不为null了,因为它指向了这块内存,此时如果线程B走到了"if (instance == null)",那么线程B其实拿到的还是一个未初始化的实例,因为这块内存还没有初始化,这就出现了问题。
指令重排序是导致出现线程不安全的直接原因,而根本原因则是对象实例化不是一个原子操作。

四、 关于原子操作

原子操作:不可划分的最小单位操作,不会被线程调度机制打断,不会有线程切换,整个操作要么不执行,一旦执行就会运行到结束。

我们来看一个简单的例子:

Object a;
Object b = new Object();
a = b;

对于"a = b" 这一操作指令,将a这个引用指向b这一对象的内存,只需要改变a的指针,因此该直接赋值操作是一个不可划分的原子操作。

再看另一个例子:

int i = 0;
i ++;

对于"i ++"这一操作指令,其实它分为三个步骤执行:

  • 读取i的值
  • 将i的值加1
  • 将新的值赋值给i

类似的还有:

boolean b = true;
b = !b;

对于这些涉及自身值的操作,由于其最终实现需要划分更小的操作单位,因此均不是原子操作。

对于非原子操作,在多线程下就可能出现线程安全问题,这也是我们的双检锁不安全的根本原因,实例化对象不是一个原子操作。

五、 实现线程安全的双检锁

我们只需要对instance加上一个volatile修饰符便可解决线程安全问题,其实就是依赖了volatile的禁止进行指令重排序的特性。

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

除此之外volatile还有一个特性就是可见性,保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。

synchronized和volatile的完美配合,便实现了线程安全的懒汉式双检锁单例模式。

Java的懒汉式双检锁单例模式相关推荐

  1. 单例模式:懒汉式 饿汉式 双检锁 登记式 枚举式 详细讲解

    单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一.这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式. 这种模式涉及到一个单一的类,该类负责创建自己的对 ...

  2. 单例模式(懒汉模式-双检锁、饿汉模式、静态内部类模式)-详细

    文章目录 前言 单例模式(懒汉模式-双检锁.饿汉模式.静态内部类模式)-详细 01 单例模式是什么? 02 单例模式的好处? 03 单例模式的三种模式 03::01 懒汉模式 03::01::01 问 ...

  3. sentinel里的双检锁

    单例模式有很多种,饿汉式,懒汉式,双检锁,公司里大部分都是选择了双检锁,其中sentinel的ContextUtil源码里就有相关的实现: protected static Context trueE ...

  4. 双重检查锁单例模式为什么要用volatile关键字?

    前言 从Java内存模型出发,结合并发编程中的原子性.可见性.有序性三个角度分析volatile所起的作用,并从汇编角度大致说了volatile的原理,说明了该关键字的应用场景:在这补充一点,分析下v ...

  5. 深入理解设计模式-单例模式(饿汉单例模式、懒汉单例模式、双锁单例模式)

    深入理解设计模式-双锁单例模式 文章目录 一.什么是单例模式 二.应用场景 三.优缺点 四.代码实现 总结 结尾 一.什么是单例模式 单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个 ...

  6. Java设计模式之创建型:单例模式

    一.什么是单例模式: 单例模式可以确保系统中某个类只有一个实例,该类自行实例化并向整个系统提供这个实例的公共访问点,除了该公共访问点,不能通过其他途径访问该实例.单例模式的优点在于: 系统中只存在一个 ...

  7. DCL双检查锁机制实现线程安全的单例设计模式

    实现线程安全的单例设计模式的三种方式: DCL双检查锁机制实现线程安全 使用静态内置类实现线程安全 使用static代码块实现线程安全 -------------------------------- ...

  8. Java中的常见的锁及其内存语义

    文章目录 为什么会有锁? JVM内存模型 没有锁会怎么样? happens-before 先行先发生原则 Java中常见的锁 synchronized 内存语义 实现原理 volatile 内存语义 ...

  9. java 静态代码块 多线程,Java多线程编程笔记10:单例模式

    立即加载:"饿汉模式" 立即加载就是指使用类的时候已经将对象创建完毕,常见的实现方法就是直接new实例化.也就是在调用方法前,实例就被创建了.示例代码如下所示: class MyO ...

最新文章

  1. 一位后端妹纸的面试总结(美团+阿里+携程+58+贝贝+招银+华为+....)
  2. 解决element-ui的表格设置固定栏后,边框线消失的bug
  3. C语言程序设计 | 模拟实现字符串操作函数:strlen, strcmp, strcpy, strcat, strchr, strstr
  4. 超过1w的Github Star大佬和他们的公众号,太强了!
  5. 一个公网ip多少钱_一个丛书书号多少钱
  6. python input函数详解_对Python3中的input函数详解
  7. 【C语言进阶】C语言实现通讯录(简易版)
  8. 【图像融合】基于脉冲神经网络PCNN实现图像融合附matlab代码
  9. GB28181国标错误码整理
  10. 我常用的几个软件的注册码
  11. matlab拟合图形边界,matlab - MATLAB 3D曲线拟合,带有附加边界 - SO中文参考 - www.soinside.com...
  12. php——三篇夯实根基第三篇
  13. Mysql报错 Error querying database. Cause java.sql.SQLSyntaxErrorException
  14. iOS 通讯录备份、恢复
  15. 高校最美图书馆!飘在水上?
  16. 挑选西瓜(决策树实现)
  17. 汽车美容店会员开卡办理html,洗车店会员卡管理系统线下线上会员一体化?
  18. 中国石油大学(北京)-《 油气藏经营管理》第二阶段在线作业
  19. 麦咖啡与360安全卫士及优化大师均有冲突
  20. 广州工商学院计算机系主任,第二次计算机系教学研讨暨专业自评动员大会

热门文章

  1. IntelliJ IDEA注释模板设置
  2. 面向NLP的AI产品方法论——如何做好“多轮对话管理”
  3. 一波四折,魔幻七面拿下腾讯 Offer!
  4. CreateThread()函数使用
  5. 程序员之天梯排行,你在哪里?
  6. python爬取音乐歌曲大全_Python爬取网易云音乐歌单歌曲
  7. 【光圈】光圈是什么意思
  8. 计算机设计与制作教学设计,计算机应用基础(五年制大专)表格的设计与制作教学设计...
  9. win10 任务栏出现 FastPicEx DeskBand
  10. 深富策略:节前最后一个交易日策略该如何