问题引入

Java中实现单例模式,一般性的做法是如下方式:

class Singleton {
private static Singleton INSTANCE = null;
private Singleton() {} public static getInstance() {
if (null == INSTANCE) { // <-- 此处如果有多个执行流同时进入,会造成多次初始化
INSTANCE = new Singleton();
}
return INSTANCE;
}
}

上述代码中,第6行处,对单例对象INSTANCE进行判空检查,如果为null,则进行初始化。

这一步在单执行流的逻辑上是没有问题的。但是当多个执行流同时运行到此处时,如果执行流a正在初始化Singleton对象,还没返回其引用,就被调度出去了,此时执行流b也会进入此处,再次对Singleton对象进行初始化。如此一来,JVM中就会存在多个Singleton实例。

因此,第7行的Singleton初始化代码块,应当作为临界区,对其访问需要加锁同步。

初步解决方案

class Singleton {
private static Singleton INSTANCE = null;
private Singleton() {} public static getInstance() {
if (null == INSTANCE) { // <-- 第1次,一般性检查,但是有并发隐患:可能有多执行流同时进入改处
synchronized(Singleton.class) {
if (null == INSTANCE) { // <-- 此处第2次检查,为了防止后续多执行流并发时,后续获取同步锁的执行流,不会再次初始化Singleton对象
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}

如上,第1次检查,用来判断是否需要对Singleton进行初始化;如果是,则先加同步锁(此时可能有多个执行流都运行到改处);获得锁之后,第2次检查Singleton对象是否已被其他并发的执行流初始化了(这个null判空检查有隐患,后续阐明);如果两次检查都通过,则表明当前执行流,是第一个进入临界区的,因此可以担负对Singleton对象初始化的责任。由于同步加锁及第2次检查的存在,后续其他的执行流,即使同时进入临界区外等待,也不会出现对Singleton对象多次初始化的问题。

以上,应该是比较完美的解决方案了。

但是,

由于对象初始化的过程并不是原子的指令,无法在单个指令周期完成,又Java编译器对指令重排序优化的存在,对象初始化的操作流程会发生变化:

原始流程:

op1:分配内存空间
op2:初始化对象
op3:将对象的引用,指向分配的内存

指令重排序优化之后的流程:

op1:分配内存空间
op2:将对象的引用,指向分配的内存
op3:初始化对象

由于对象初始化流程的非原子性,当前执行流很可能在新流程的op2->op3这一步被调度出去,进而导致JVM中存在着一个已开辟内存空间、但是未初始化的Singleton实例。如果此时,其他调度进来的执行流使用了这个残缺的Singleton实例,很有可能因为数据异常引发运行时错误。

完善后的解决方案

为此,我们需要一个机制,来阻止编译器对指令的重排序——这就是关键字 volatile

加了 volatile 关键字的变量,编译器不会对其初始化指令进行重排序优化。因此就避免了上述的问题发生。

class Singleton {
private static volatile Singleton INSTANCE = null; // <-- 禁止指令重排序
private Singleton() {} public static getInstance() {
if (null == INSTANCE) { // <-- 第1次,一般性检查,但是有并发隐患:可能有多执行流同时进入改处
synchronized(Singleton.class) {
if (null == INSTANCE) { // <-- 此处第2次检查,为了防止后续多执行流并发时,后续获取同步锁的执行流,不会再次初始化Singleton对象
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}

后记

我还想到一个不利用 Java 的 volatile 特性的方案:

class Singleton {
private static Singleton INSTANCE = null;
private static constructed = false; // <-- 用一个标记变量
private Singleton() {} public static getInstance() {
if (!constructed) { // <-- 第1次,一般性检查,但是有并发隐患:可能有多执行流同时进入改处
synchronized(Singleton.class) {
if (!constructed) { // <-- 此处第2次检查,为了防止后续多执行流并发时,后续获取同步锁的执行流,不会再次初始化Singleton对象
INSTANCE = new Singleton();
constructed = true; // <-- 我没有探究这里,会不会出现指令重排序的情况
}
}
}
return INSTANCE;
}
}

Java单例-双重检查锁的更多相关文章

  1. Java中的双重检查锁(double checked locking)

    最初的代码 在最近的项目中,写出了这样的一段代码 private static SomeClass instance; public SomeClass getInstance() { if (nul ...

  2. 从学习“单例模式”学到的Java知识:双重检查锁和延迟初始化

    一切真是有缘,上午刚刚看完单例模式,还在为其中的代码块同步而兴奋,下午就遇见这篇文章:双重检查锁定与延迟初始化.我一看,文章开头语出惊人,说这是一种错误的优化,我说,难道上午学的东西下午就过时了吗?仔 ...

  3. 单例模式中用volatile和synchronized来满足双重检查锁机制

    背景:我们在实现单例模式的时候往往会忽略掉多线程的情况,就是写的代码在单线程的情况下是没问题的,但是一碰到多个线程的时候,由于代码没写好,就会引发很多问题,而且这些问题都是很隐蔽和很难排查的. 例子1 ...

  4. 双重检查锁实现单例(java)

    单例类在Java开发者中非常常用,但是它给初级开发者们造成了很多挑战.他们所面对的其中一个关键挑战是,怎样确保单例类的行为是单例?也就是说,无论任何原因,如何防止单例类有多个实例.在整个应用生命周期中 ...

  5. Java基础教程:多线程杂谈——双重检查锁与Volatile

    Java基础教程:多线程杂谈——双重检查锁与Volatile 双重检查锁 有时候可能需要推迟一些高开销的对象初始化操作,并且只有在使用这些对象时才进行初始化.此时程序员可能会采用延迟初始化.但要正确实 ...

  6. 【Java学习笔记】线程安全的单例模式及双重检查锁—个人理解

    搬以前写的博客[2014-12-30 16:04] 在web应用中服务器面临的是大量的访问请求,免不了多线程程序,但是有时候,我们希望在多线程应用中的某一个类只能新建一个对象的时候,就会遇到问题. 首 ...

  7. Java盲点:双重检查锁定及单例模式

    尊重原创: http://gstarwd.iteye.com/blog/692937 2004 年 5 月 01 日 所有的编程语言都有一些共用的习语.了解和使用一些习语很有用,程序员们花费宝贵的时间 ...

  8. 写一个安全的Java单例

    单例模式可能是我们平常工作中最常用的一种设计模式了.单例模式解决的问题也很常见,即如何创建一个唯一的对象.但想安全的创建它其实并不容易,还需要一些思考和对JVM的了解. 1.首先,课本上告诉我,单例这 ...

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

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

随机推荐

  1. RHCAS_DAY01

    世界三大云厂商: 第一:亚马逊,AWS 第二:微软,Azure 第三:中国,阿里云,在全球15个地区建立的200多个数据中心 https://www.aliyun.com/ #阿里云地址 云计算三种模 ...

  2. 《Python Cookbook v3.0.0》Chapter1 数据结构和算法

    感谢: https://github.com/yidao620c/python3-cookbook 如有侵权,请联系我整改. 本文章节会严格按照原书(以便和原书对照,章节标题可能会略有修改),内容会有 ...

  3. 图 邻接表 邻接矩阵 BFS生成树 DFS生成树

  4. 黑马JVM教程——自学笔记(二)

    三.垃圾回收 3.1.如何判断对象可以回收 3.1.1 引用计数法 弊端:循环引用时,两个对象的计数都为1,导致两个对象都无法被释放 3.1.2 可达性分析算法 JVM中的垃圾回收器通过可达性分析来探 ...

  5. 实现动态加载一个 JavaScript 资源

    var script = document.createElement("script"); var head = document.getElementsByTagName(&q ...

  6. C#:[StructLayout(LayoutKind.Sequential)]

    参考网址: https://blog.csdn.net/hongkonglife/article/details/23422857 结构体是由若干成员组成的.布局有两种1.Sequential,顺序布 ...

  7. ASP.NET Core:中间件

    一.什么是中间件 我们都知道,任何的一个web框架都是把http请求封装成一个管道,每一次的请求都是经过管道的一系列操作,最终才会到达我们写的代码中.而中间件就是用于组成应用程序管道来处理请求和响应的 ...

  8. .Net Core 踩坑记录--程序独立发布 无法运行

    背景 创建.net Core3.1 的Console程序 点击发布 选择独立部署模式 目标电脑 Win10 x64 未安装任何.Net SDK 现象 发布的程序 点击运行没有反应 或是直接闪退 解决 ...

  9. C#中的集合类

    集合相当于容器,用于将一系列相似的项组合在一起. 集合可以分为泛型集合类和非泛型集合类. 多数集合类都是派生自ICollection.IComparer.IEnumerable.IList.IDict ...

  10. C#异步编程 Task await的理解

    async/await是C#5.0中推出的,先上用法: static void Main(string[] args) { Console.WriteLine("-------主线程启动-- ...