DCL,即Double Check Lock,中卫双重检查锁定。其实DCL很多人在单例模式中用过,LZ面试人的时候也要他们写过,但是有很多人都会写错。他们为什么会写错呢?其错误根源在哪里?有什么解决方案?下面就随LZ一起来分析
问题分析
我们先看单例模式里面的懒汉式:
publicclassSingleton{privatestaticSingleton singleton;privateSingleton(){}publicstaticSingleton getInstance(){if(singleton ==null){ singleton =newSingleton();}return singleton;}}
我们都知道这种写法是错误的,因为它无法保证线程的安全性。优化如下:
publicclassSingleton{privatestaticSingleton singleton;privateSingleton(){}publicstaticsynchronizedSingleton getInstance(){if(singleton ==null){ singleton =newSingleton();}return singleton;}}
优化非常简单,就是在getInstance方法上面做了同步,但是synchronized就会导致这个方法比较低效,导致程序性能下降,那么怎么解决呢?聪明的人们想到了双重检查 DCL:
publicclassSingleton{privatestaticSingleton singleton;privateSingleton(){}publicstaticSingleton getInstance(){if(singleton ==null){// 1synchronized(Singleton.class){// 2if(singleton ==null){// 3 singleton =newSingleton();// 4}}}return singleton;}}
就如上面所示,这个代码看起来很完美,理由如下:
- 如果检查第一个singleton不为null,则不需要执行下面的加锁动作,极大提高了程序的性能;
- 如果第一个singleton为null,即使有多个线程同一时间判断,但是由于synchronized的存在,只会有一个线程能够创建对象;
- 当第一个获取锁的线程创建完成后singleton对象后,其他的在第二次判断singleton一定不会为null,则直接返回已经创建好的singleton对象;
通过上面的分析,DCL看起确实是非常完美,但是可以明确地告诉你,这个错误的。上面的逻辑确实是没有问题,分析也对,但是就是有问题,那么问题出在哪里呢?在回答这个问题之前,我们先来复习一下创建对象过程,实例化一个对象要分为三个步骤:
- 分配内存空间
- 初始化对象
- 将内存空间的地址赋值给对应的引用
但是由于重排序的缘故,步骤2、3可能会发生重排序,其过程如下:
- 分配内存空间
- 将内存空间的地址赋值给对应的引用
- 初始化对象
如果2、3发生了重排序就会导致第二个判断会出错,singleton != null,但是它其实仅仅只是一个地址而已,此时对象还没有被初始化,所以return的singleton对象是一个没有被初始化的对象,如下:
按照上面图例所示,线程B访问的是一个没有被初始化的singleton对象。
通过上面的阐述,我们可以判断DCL的错误根源在于步骤4:
singleton =newSingleton();
知道问题根源所在,那么怎么解决呢?有两个解决办法:
- 不允许初始化阶段步骤2 、3发生重排序。
- 允许初始化阶段步骤2 、3发生重排序,但是不允许其他线程“看到”这个重排序。
解决方案
解决方案依据上面两个解决办法即可。
基于volatile解决方案
对于上面的DCL其实只需要做一点点修改即可:将变量singleton生命为volatile即可:
publicclassSingleton{//通过volatile关键字来确保安全privatevolatilestaticSingleton singleton;privateSingleton(){}publicstaticSingleton getInstance(){if(singleton ==null){synchronized(Singleton.class){if(singleton ==null){ singleton =newSingleton();}}}return singleton;}}
当singleton声明为volatile后,步骤2、步骤3就不会被重排序了,也就可以解决上面那问题了。
基于类初始化的解决方案
该解决方案的根本就在于:利用classloder的机制来保证初始化instance时只有一个线程。JVM在类初始化阶段会获取一个锁,这个锁可以同步多个线程对同一个类的初始化。
publicclassSingleton{privatestaticclassSingletonHolder{publicstaticSingleton singleton =newSingleton();}publicstaticSingleton getInstance(){returnSingletonHolder.singleton;}}
这种解决方案的实质是:运行步骤2和步骤3重排序,但是不允许其他线程看见。
Java语言规定,对于每一个类或者接口C,都有一个唯一的初始化锁LC与之相对应。从C到LC的映射,由JVM的具体实现去自由实现。JVM在类初始化阶段期间会获取这个初始化锁,并且每一个线程至少获取一次锁来确保这个类已经被初始化过了。
http://cmsblogs.com/?p=2161
相关推荐
Java内存模型详解JMM.docx
深入Java内存模型-JMM。。。。。。。。。。。。。。。。。。
java内存模型jmm
深入理解 Java 内存模型,由程晓明编著,深入理解java内存模型JMM
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。...
Java 内存模型的抽象 4 重排序 6 处理器重排序与内存屏障指令 7 happens-before 10 重排序 13 数据依赖性 13 as-if-serial 语义 13 程序顺序规则 15 重排序对多线程的影响 15 顺序一致性 19 数据竞争与顺序...
Java内存模型的抽象 重排序 处理器重排序与内存屏障指令 happens-before 重排序 数据依赖性 as-if-serial 语义 程序顺序规则 重排序对多线程的影响 顺序一致性 数据竞争与顺序一致性保证 顺序一致性内存模型 同步...
在说Java内存模型之前,我们先说一下Java的内存结构,也就是运行时的数据区域:Java虚拟机在执行Java程序的过程中,会把它管理的内存划分为几个不同的数据区域,这些区域都有各自的用途、创建时间、销毁时间
深入理解Java内存模型
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定
从抽象的角度来看,JMM是这样定义线程和主内存之间的抽象关系的:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的...
Java运行时内存模型图
Java内存模型及Volatile底层实现原理
介绍java的内存管理方式和特点 1.JMM 简介 2.堆和栈 3.本机内存 4.防止内存泄漏
由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题:由于 CPU 执行速度很快,而从内存读取数据和向内存写入数据的过程跟 CPU 执行指令的速度比起来要差几个数量级,因此如果任何时候...
深入理解并发可见性、有序性、原子性与JMM内存模型深入理解并发可见性、有序性、原子性与JMM内存模型深入理解并发可见性、有序性、原子性与JMM内存模型深入理解并发可见性、有序性、原子性与JMM内存模型深入理解并发...
Java Memory Model简称JMM, 是一系列的Java虚拟机平台对开发者提供的多线程环境下的内存可见性、是否可以重排序等问题的无关具体平台的统一的保证。(可能在术语上与Java运行时内存分布有歧义,后者指堆、方法区、...
Java内存模型 1.JMM简介 2.堆和栈 3.本机内存 4.防止内存泄漏
c#、java、php等多语言解决方案源代码 Wafer - 快速构建具备弹性能力的微信小程序 https://github.com/tencentyun/wafer 重要: 1.第二步,可以在5分钟内实现; 2.成本3元(腾讯云支持微信小程序2017年推广期间,3...