前言

本文我们从一个问题出发来进行探究关于volatile的应用。

问题:单例模式的双重检查锁模式为什么必须加 volatile?

什么是单例模式

单例模式指的是,保证一个类只有一个实例,并且提供一个可以全局访问的入口。

为什么需要使用单例模式

那么我们为什么需要单例呢?其中**一个理由,那就是为了节省内存、节省计算。**因为在很多情况下,我们只需要一个实例就够了,如果出现更多的实例,反而纯属浪费。

下面我们举一个例子来说明这个情况,以一个初始化比较耗时的类来说,代码如下所示:

public class ExpensiveResource {public ExpensiveResource() {field1 = // 查询数据库field2 = // 然后对查到的数据做大量计算field3 = // 加密、压缩等耗时操作}
}

这个类在构造的时候,需要查询数据库并对查到的数据做大量计算,所以在第一次构造时,我们花了很多时间来初始化这个对象。但是假设数据库里的数据是不变的,我们就可以把这个对象保存在内存中,那么以后开发的时候就可以直接用这同一个实例了,不需要再次构建新实例。如果每次都重新生成新的实例,则会造成更多的浪费,实在没有必要。

接下来看看需要单例的第二个理由,那就是为了保证结果的正确。比如我们需要一个全局的计数器,用来统计人数,如果有多个实例,反而会造成混乱。

另外呢,就是为了方便管理。很多工具类,我们只需要一个实例,那么我们通过统一的入口,比如通过 getInstance 方法去获取这个单例是很方便的,太多实例不但没有帮助,反而会让人眼花缭乱。

一般单例模式的类结构如下图所示:有一个私有的 Singleton 类型的 singleton 对象;同时构造方法也是私有的,为了防止他人调用构造函数来生成实例;另外还会有一个 public 的 getInstance 方法,可通过这个方法获取到单例。

双重检查锁模式的写法

单例模式有多种写法,在设计模式(一)——单例模式 一文中我们学过饿汉式和懒汉式两种方式,现在我们重点介绍一下和 volatile 强相关的双重检查锁模式的写法,代码如下所示:

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

上述getInstance 方法中首先进行了一次 if (singleton == null) 的检查,然后是 synchronized 同步块,然后又是一次 if (singleton == null) 的检查,最后是 singleton = new Singleton() 来生成实例。

我们进行了两次 if (singleton == null) 检查,这就是“双重检查锁”这个名字的由来。这种写法是可以保证线程安全的,假设有两个线程同时到达 synchronized 语句块,那么实例化代码只会由其中先抢到锁的线程执行一次,而后抢到锁的线程会在第二个 if 判断中发现 singleton 不为 null,所以跳过创建实例的语句。再后面的其他线程再来调用 getInstance 方法时,只需判断第一次的 if (singleton == null) ,然后会跳过整个 if 块,直接 return 实例化后的对象。

这种写法的优点是不仅线程安全,而且延迟加载、效率也更高。

思考一个问题,“为什么要 double-check?去掉任何一次的 check 行不行?”

我们先来看第二次的 check,这时你需要考虑这样一种情况,有两个线程同时调用 getInstance 方法,由于 singleton 是空的 ,因此两个线程都可以通过第一重的 if 判断;然后由于锁机制的存在,会有一个线程先进入同步语句,并进入第二重 if 判断 ,而另外的一个线程就会在外面等待。

不过,当第一个线程执行完 new Singleton() 语句后,就会退出 synchronized 保护的区域,这时如果没有第二重 if (singleton == null) 判断的话,那么第二个线程也会创建一个实例,此时就破坏了单例,这肯定是不行的。

而对于第一个 check 而言,如果去掉它,那么所有线程都会串行执行,效率低下,所以两个 check 都是需要保留的。

在双重检查锁模式中为什么需要使用 volatile 关键字

我们在双重检查锁模式中,给 singleton 这个对象加了 volatile 关键字,那**为什么要用 volatile 呢?**主要就在于 singleton = new Singleton() ,它并非是一个原子操作,事实上,在 JVM 中上述语句至少做了以下这 3 件事:

  1. 第一步是给 singleton 分配内存空间;
  2. 然后第二步开始调用 Singleton 的构造函数等,来初始化 singleton;
  3. 最后第三步,将 singleton 对象指向分配的内存空间(执行完这步 singleton 就不是 null 了)。

这里需要留意一下 1-2-3 的顺序,因为存在指令重排序的优化,也就是说第2 步和第 3 步的顺序是不能保证的,最终的执行顺序,可能是 1-2-3,也有可能是 1-3-2。

如果是 1-3-2,那么在第 3 步执行完以后,singleton 就不是 null 了,可是这时第 2 步并没有执行,singleton 对象未完成初始化,它的属性的值可能不是我们所预期的值。假设此时线程 2 进入 getInstance 方法,由于 singleton 已经不是 null 了,所以会通过第一重检查并直接返回,但其实这时的 singleton 并没有完成初始化,所以使用这个实例的时候会报错,详细流程如下图所示:

线程 1 首先执行新建实例的第一步,也就是分配单例对象的内存空间,由于线程 1 被重排序,所以执行了新建实例的第三步,也就是把 singleton 指向之前分配出来的内存地址,在这第三步执行之后,singleton 对象便不再是 null。

这时线程 2 进入 getInstance 方法,判断 singleton 对象不是 null,紧接着线程 2 就返回 singleton 对象并使用,由于没有初始化,所以报错了。最后,线程 1 “姗姗来迟”,才开始执行新建实例的第二步——初始化对象,可是这时的初始化已经晚了,因为前面已经报错了。

使用了 volatile 之后,相当于是表明了该字段的更新可能是在其他线程中发生的,因此应确保在读取另一个线程写入的值时,可以顺利执行接下来所需的操作。在 JDK 5 以及后续版本所使用的 JMM 中,在使用了 volatile 后,会一定程度禁止相关语句的重排序,从而避免了上述由于重排序所导致的读取到不完整对象的问题的发生。

使用 volatile 的意义主要在于它可以防止避免拿到没完成初始化的对象,从而保证了线程安全。

总结

我们首先介绍了什么是单例模式,以及为什么需要使用单例模式,然后介绍了双重检查锁模式这种写法,以及面对这种写法时为什么需要 double-check,为什么需要用 volatile?最主要的是为了保证线程安全。

java并发编程(二十六)——单例模式的双重检查锁模式为什么必须加 volatile?相关推荐

  1. java并发编程(三十五)——公平与非公平锁实战

    前言 在 java并发编程(十六)--锁的七大分类及特点 一文中我们对锁有各个维度的分类,其中有一个维度是公平/非公平,本文我们来探讨下公平与非公平锁. 公平|非公平 首先,我们来看下什么是公平锁和非 ...

  2. 【并发编程二十】协程(coroutine)_协程库

    [并发编程二十]协程(coroutine) 一.线程的缺点 二.协程 三.优点 四.个人理解 五.协程库 1.window系统 2.unix系统(包括linux的各个版本) 2.1.makeconte ...

  3. Java 并发编程(二):如何保证共享变量的原子性?

    线程安全性是我们在进行 Java 并发编程的时候必须要先考虑清楚的一个问题.这个类在单线程环境下是没有问题的,那么我们就能确保它在多线程并发的情况下表现出正确的行为吗? 我这个人,在没有副业之前,一心 ...

  4. 单例模式之双重检查锁(double check locking)的发展历程

    不安全的单例 没有注意过多线程安全问题的时候,我们的单例可能是这样的: public final class Singleton {private static Singleton instance; ...

  5. 并发编程(十六)——java7 深入并发包 ConcurrentHashMap 源码解析

    以前写过介绍HashMap的文章,文中提到过HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容 ...

  6. Java单例模式中双重检查锁的问题

    单例创建模式是一个通用的编程习语.和多线程一起使用时,必需使用某种类型的同步.在努力创建更有效的代码时,Java 程序员们创建了双重检查锁定习语,将其和单例创建模式一起使用,从而限制同步代码量.然而, ...

  7. 聊聊高并发(二十六)解析java.util.concurrent各个组件(八) 理解CountDownLatch闭锁

    CountDownLatch闭锁也是基于AQS实现的一种同步器,它表示了"所有线程都等待,直到锁打开才继续执行"的含义.它和Semaphore的语意不同, Semaphore的获取 ...

  8. Java多线程学习二十六:原子类是如何利用 CAS 保证线程安全的?

    什么是原子类,以及它有什么作用. 在编程领域里,原子性意味着"一组操作要么全都操作成功,要么全都失败,不能只操作成功其中的一部分".而 java.util.concurrent.a ...

  9. Java并发编程(二十三)------并发设计模式之生产者消费者模式

    参考文章:Java实现生产者消费者问题与读者写者问题 目录 1. 生产者消费者问题 1.1 wait() / notify()方法 1.2 await() / signal()方法 1.2.1 对sy ...

最新文章

  1. 手游研发CJ抱大腿指南
  2. Android——怎么引入okhttp3
  3. 【视频】vue动态绑定css样式
  4. SetRegistryKey的作用
  5. Orleans简单配置
  6. adb 切换默认桌面_公告 | 武林外传手游官方服务器全面开放桌面版体验
  7. 虚拟机与主机串口通信(主机与主机)
  8. springmvc+json 前后台数据交互
  9. 值类型和引用类型小解
  10. xdb 服务_如何删除默认的XPT和XDB这两个服务
  11. redis突然宕机数据会丢失吗
  12. 企业电子招投标系统简介 招投标系统源码 定制化服务 二次开发 java招投标系统 招投标系统功能设计
  13. 【数据库基础】正则化(Normalization)P1:UNF、1NF、2NF、3NF
  14. 计算机知识产权有哪些,计算机类专利有哪些可以申请
  15. js对文字批注_HTML 页面添加批注 - JavaScript - ITeye
  16. 【搞定GTD】当前的GTD状态测试
  17. Android 10 和Android 11 适配采坑 实践篇
  18. 关于“分节符(下一页)”和“分节符(奇数页)”的问题
  19. 五十四、HBase的协处理器
  20. 毫无保留!珍藏多年的“学习”网站...

热门文章

  1. Intellij idea 2020设置经典样式(背景为黑色Darcula)
  2. 微信小程序开发,微信小程序轻松实现微信支付及微信提现到零钱功能
  3. 解决git在push时报错fatal: unable to access ‘https://github.com/sup0C/a.git/‘: errno 10054
  4. iOS中常用的几种延时加载/执行的处理办法
  5. STM8L051之通过ADC1与DMA读取内部参考电压,求取VDD电源电压---库函数版
  6. vlc-android源码git下载
  7. 语音识别技术的前世今生之前世
  8. phpstudyv8集成环境下,用Dwcs6链接数据库失败并显示:your php server doesnot have the Mysql module……
  9. 基于C++模板类编程数据结构图的操作---注意error:2248的解析
  10. 影音视频播放中常见八问题