问题引入

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. Java面向对象07——封装

    封装 (补充 this关键字):  package oop.demon01.demon03; ​ /*    封装的意义:        1. 提高程序的安全性,保护代码        2. 隐藏代码 ...

  2. postman之断言

    1 (状态码断言)和(返回内容断言)

  3. 太细了!阿里十年技术专家联合打造“最新”Jetpack强化实战手册

    前言 提到Android架构,我们首先想到的是MVC,MVP,MVVM.他们主要是针对视图和模型的.随着Android的发展,从原来的框架很少,全是自己动手撸.到现在框架越来越多,选型也越来越多,导致 ...

  4. Linux常见问题解决方案

    1.Kali2020添加BCM43142的网卡驱动 来源:https://www.fujieace.com/kali-linux/wifi-drive.html 我只是执行了第三步:安装网卡驱动,即: ...

  5. JAVA基础语法:java编程规范和常用数据类型(转载)

    JAVA基础语法:java编程规范和常用数据类型 摘要 本文主要介绍了最基本的java程序规则,和常用数据类型,其中侧重说了数组的一些操作. 面向java编程 java是纯面向对象语言,所有的程序都要 ...

  6. Shell-10-标准输入输出错误

    标准输入输出和错误 标准输入.输出和错误 重定向符号 示例 1 1 标准输出 2 错误输出 2 标准输出和错误输出同时定向到一个文件中 >share.txt 2>&1 3 > ...

  7. 安全工具推荐之sqlmap tamper&sqlmap api

    我发现总有一些人喜欢问sqlmap的tamper脚本,问完工具问参数,问完参数问脚本...... 你这个问题问的水平就很艺术,让我一时不知从何说起...... 说一下在sqlmap的使用过程中,个人了 ...

  8. Kali 2.0 安装教程

    本文适合KALI初学者,将详细介绍Kali Linux 2.0的安装过程. 首先我们到KALI的官网下载镜像,大家可以自己选择下载32或64位的KALI 2.0系统. KALI 官网:https:// ...

  9. 服务器受到网络攻击时,如何获取请求客户端的真实 IP?

    网络攻击 前不久公司遭受了一次网络攻击. 早晨刚到公司,就发现登录接口的调用次数飙升,很快就确认是被恶意攻击,让安全部门做网关入口针对对方 IP 加了限制. 并统一对所有的 IP 加了调用的频率限制. ...

  10. 公司新来了一个质量工程师,说团队要保证 0 error,0 warning

    摘要:静态代码检查又称为静态程序分析,是指在不运行计算机程序的条件下,进行程序分析的方法. 本文分享自华为云社区<公司新来了一个质量工程师,说团队要保证 0 error,0 warning> ...