问题引入

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流程控制06——break continue

  2. C语言中变参函数传参探究

    背景引入 近期在看一本书,叫做<嵌入式C语言自我修养>,写的内容对我帮助很大,是一本好书.在第6章,GNU C编译器扩展语法精讲一节,这本书给出了一些变参函数的例子: //1.变参函数初体 ...

  3. Go语言笔记[实现一个Web框架实战]——EzWeb框架(一)

    Go语言笔记[实现一个Web框架实战]--EzWeb框架(一) 一.Golang中的net/http标准库如何处理一个请求 func main() { http.HandleFunc("/& ...

  4. spring中的组合模式

    org.springframework.cache.support.CompositeCacheManager /* * Copyright 2002-2016 the original author ...

  5. Oracle 数据库的导入与导出

    1.导入 打开cmd,用管理员登录:sqlplussys as sysdba密码不用输: 创建表空间:create tablespace tablespaceName datafile 'E:\tab ...

  6. 题解 v

    传送门 考场上只会爆搜--觉得重复状态其实有很多但不知道怎么记忆化,结果-- 对于类似这样n不算太小但只有二三十,而重复状态极多的题其实也是可以跑状压/记搜的,状态可以开map存 然后就是爆搜,就没什 ...

  7. SpringBoot集成Druid

    maven <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-b ...

  8. 安装 iperf和服务器之间测速

    安装 # https://downloads.es.net/pub/iperf/iperf-3.1.3.tar.gz wget https://iperf.fr/download/source/ipe ...

  9. 使用DOM方法来遍历一个文档

    问题 你有一个HTML文档要从中提取数据,并了解这个HTML文档的结构. 方法 将HTML解析成一个Document之后,就可以使用类似于DOM的方法进行操作.示例代码: File input = n ...

  10. (1)hadoop之----linux配置jdk环境

    首先Linux中应有jdk包 运用flashFXP上传文件,xshell连接linux系统 我一般将文件放在个人目录下softwear目录 ,软件装在个人目录下app目录 cd software    ...