单例模式双重检查(DCL)引发的多线程问题
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式双重检查(DCL)引发的多线程问题相关的知识,希望对你有一定的参考价值。
参考技术A 首先先看一个使用双重检查的单例模式:双检锁机制的出现确实是解决了多线程并行中不会出现重复new对象,而且也实现了懒加载,但很遗憾的是 instance = new DoubleCheckedLock()在编译器下实现是有过程的
过程1. 给新的实体instance分配内存
过程2. 调用DoubleCheckedLock的构造函数给instance初始化
过程3. 将incetance指向分配的内存空间
在这三个过程之间,由于JVM的“优化”机制,如果出现多线程并发访问的情况,就会出现A线程在执行过程2的时候,已经分配好了内存,此时想要进入过程3,不巧的是,线程B进入了过程2,此时instance并不为null(内存已经被分配),导致线程B此时不执行创建对象的语句,改为直接返回一个未初始化的DoubleCheckedLock对象!
可以使用volatile关键字来定义该变量的语义,使得每次修改instance后,线程工作内存强制刷新到主存中,禁止了JVM的指令重排序,防止了该问题
使用volatile另一个方面则使instance对于其他线程可见,这里被锁住的是DoubleCheckedLock.class而非instance,如果不使用volatile,则有可能引发当前变量更改后只存在于自身线程的工作内存中(即未同步到主存)而引发的多个线程创建多个实例的后果,导致单例模式失去意义。
单例模式DCL式以及Java指令无序性解析
这两天有点咸鱼,手头没项目写,今天翻了翻刘望舒大神的安卓进阶之光,刚好看到对volatile关键字的解析。自己以前很少涉及到关键字,直到它的存在但是没有了解过,今晚花了点时间把它搞清楚,就看到了单例模式中双重检查模式的应用。这里借助单例模式的双重检查模式讲讲。写写自己的一点小看法。
单例模式的目的是在程序执行期间,对实现单例模式的类,只存在唯一的一个实例。主要是针对某些创建销毁时特别吃内存的类或者想要保持数据同步的类。
但单例模式,很多时候碰到了多线程的问题,于是可爱的程序猿们想出了各种方法,包括这里说的这种双重检查模式DCL。先贴代码
这里先提一提synchronized关键字,这个关键字是指同步代码块,即使用类或对象(在这里是Singleton.class)对花括号内的代码块进行同步,保证这个代码块在任何时间点最多只能有一个线程在执行。
那么这么写的目的是什么呢?看看如果不这么写,有什么不好的地方,同样贴代码
这是最懒的懒汉式。线程完全不安全。为什么呢?因为如果多个线程都调用了getInstance()这个方法,那么根据JVM的线程分配,可能会出现,第一个线程执行过if判断句,便让出时间片,第二个线程同样判断if语句,这时两个线程都进入了if语句块中,那么会new出两个实例,单例模式fail。那么为了解决这个问题,最初的思路就是使用synchronized这个关键字,对这个getInstance()进行同步(synchronized可以修饰方法,使方法同步),这个方法可行,但是这会降低执行效率,因为每次调用getInstance,都要进行同步锁的获取以及判定。那么我们自然会想,那就同步代码块就好了,于是有了这一种。
或者这一种
看起来很有道理,仔细想想,觉得事情还是不太对。先说第一种,第一种很有可能两个线程和之前一样,判断了instance==null,都进入了if块,那岂不是一样,那就只是new的时候保证了同步而已,其实还是没有卵用。第二种,行吧,倒是解决了问题,但是那岂不是跟把synchronized放在方法一样,每次进来都要做判断。于是我们想了想办法,把这两种集合了一下,变成了……这样!
看起来就很有道理,既实现了创建对象时的同步,又很大程度上解决了synchronized每次都要执行的效率。然而,还是有问题。这里就涉及到Java的一个特性。
为了实现“一次编译处处执行”的承诺,Java使用了虚拟机JVM的概念,将计算机真正的硬件进行了封装,提供了虚拟机这个概念。其中会有虚拟的寄存器、内存、处理器等等一系列虚拟概念。同时为了保证其代码的执行效率,JVM有一套处理器的指令执行算法,它允许在不改变程序结果的条件下,对指令执行的顺序进行调换,称为重排序。这在单线程的情况下没毛病,但是一旦涉及多线程操作,就会出现各种无法预知的错误。比如说,这里涉及的对象的创建。
JVM中对象的创建涉及多个指令,比如分配内存、堆内存中对象的初始化、返回堆内存中的引用等等。由于JVM的处理器执行指令的无序,创建对象时很有可能先在堆内存中开辟了一个空间,然后把它返回,然后再进行初始化。那这和我们说的这个单例模式有什么关系呢?设想这样一个情况。线程1已经进入了synchronized块中,并返回了对象的引用,但是初始化只执行了一半,这个时候,线程2到达了第一个if判断句中,它一看,哇,不null欸。直接返回了instance。这岂不是gg?我们拿到了一个初始化一半的instance,它要是一个人,那有可能少了一条腿。
那怎么解决呢?秘密武器来了—volatile!这个关键字用于修饰一个变量,使这个变量具有可见性、有序性(不保证原子性)。我们这里只讲有序性,不展开讲原子性和可见性。有序性就是用于克服JVM的指令无序性,它使得指令按照它原本的顺序执行,比如对象的创建,它会乖乖的,开辟内存,初始化,返回引用;于是代码变成了这样
这就是我们一开始贴出来的代码了。非常好的双重检查模式。
PS:关于单例模式还有很多种写法,个人认为这个写法并不是最好的,实现起来不顺手(也许是个人原因),而且据说新版JVM已经对这个问题已经进行了优化。单例模式完全可以写成饿汉或者登记式(内部类),也许会更好。
以上是关于单例模式双重检查(DCL)引发的多线程问题的主要内容,如果未能解决你的问题,请参考以下文章