DCL:(Double Check Lock),双重判断锁, 要知道DCL的由来,先从单例模式说起。
/**
* 单例模式 -- 饿汉式
*/
public class Singleton01 {
private static final Singleton01 INSTANCE = new Singleton01();
private Singleton01 (){
}
public static Singleton01 getInstance() {
return INSTANCE;
}
public static void main(String[] args) {
Singleton01 instance1 = getInstance();
Singleton01 instance2 = getInstance();
System.out.println(instance1 == instance2);
}
}
饿汉式就是甭管三七二十一,我上来就先把这个对象new出来,构造方法是private的,别人创建不了,通过一个静态的getInstance()方法获取实例,这样能够保证我每次获取的对象都是同一个,这是单例最简单的写法。
这时候有人说了,你这写的不咋地啊,我还没用你这个对象呢,你就给我new出来了,太浪费空间了。
/**
* 单例模式--懒汉式
*/
public class Singleton02 {
private static Singleton02 INSTANCE;
private Singleton02(){
}
public static Singleton02 getInstance(){
if (INSTANCE == null) {
try {
// 业务代码
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton02();
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()-> System.out.println(Singleton02.getInstance().hashCode())).start();
}
}
}
饱汉式是什么时候使用,我什么时候再给你创建,对象不为空我就直接返回。
这时候又有人说了,你这不对呀,在多线程的情况下是有可能创建多个对象的,什么意思呢?比如有两个线程,第一个线程进来判断INSTANCE是否为空,第一次进来肯定为空,继续执行,在执行业务代码还没有new对象的时候,第二个线程进来,判断INSTANCE是否为空,依然为空,继续往下执行,这时候线程一new了一个对象返回,线程二也继续执行new了一个对象返回,一共创建了两个对象,从上面的程序也能验证出来,hashCode不一致说明不是同一个对象。
/**
* 单例模式--升级版
*/
public class Singleton03 {
private static Singleton03 INSTANCE;
private Singleton03(){
}
public static synchronized Singleton03 getInstance(){
if (INSTANCE == null) {
try {
// 业务代码
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton03();
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()-> System.out.println(Singleton03.getInstance().hashCode())).start();
}
}
}
我在方法上加了一把锁,每个线程进来必须先拿到这把锁才能执行里面的代码,同一时刻,只能有一个线程在执行,执行完的时候一定能new出一个对象,释放锁后,第二个线程才能进来,判断的时候这个对象一定不为空,在多线程的情况下能够保证对象时唯一的。
这时候又有人问了,你这锁加到方法上了,锁的粒度太大了,我只想在需要加锁的地方加锁行不行。
/**
* 单例模式--降低锁粒度
*/
public class Singleton04 {
private static Singleton04 INSTANCE;
private Singleton04(){
}
public static Singleton04 getInstance(){
// 业务代码
if (INSTANCE == null) {
synchronized (Singleton04.class) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton04();
}
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()-> System.out.println(Singleton04.getInstance().hashCode())).start();
}
}
}
看下上面这段代码,先判断对象是否为空,为空你再上锁,这个能解决多线程下的数据不一致性问题吗?看下运行结果
很显然,还是不能保证多线程下的数据不一致性问题,怎么回事呢?
当线程一进入方法判断对象是否为空,为空,然后第一个线程停了,第二线程进来判断是否为空,为空,继续执行,new了一个对象后返回释放锁,线程一拿到锁后继续执行,又new了一个对象。所以终于诞生了美团问的这个写法,DCL。
/**
* 单例模式--DCL
*/
public class Singleton05 {
private static volatile Singleton05 INSTANCE;
private Singleton05(){
}
public static Singleton05 getInstance(){
// 业务代码
if (INSTANCE == null) {
synchronized (Singleton05.class) {
if (INSTANCE == null) { // 双重验证
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton05();
}
}
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()-> System.out.println(Singleton05.getInstance().hashCode())).start();
}
}
}
以上这段代码就是DCL,能够保证多线程下的数据不一致性问题,为什么?
线程一进入到这段代码,判断是否为空,为空,这时候线程一进入到if方法,线程一停了,线程二进来,判断是否为空,为空,继续执行,拿到锁,再判断为空吗?为空,new对象返回,释放锁,线程一继续执行,拿到锁,判断对象为空吗?不为空!返回。
这里又有人会问,你代码里第一个判断有啥用?上来直接上锁不就完了嘛!假如有1万个线程进来,如果没有第一层判断,那么这1万个线程都要去获取锁,效率太低!如果有第一层判断,有一个线程执行完了,其他线程我就不需要再去获取锁了,这就是效率问题。
但这不是美团问的不是这个,而是问你,**DCL需要加volatile
吗?**答案是肯定的,一定要加,为什么?
这里说的就是上篇文章说的volatile的第二个作用,禁止指令重排序。
说到禁止指令重排序又得先说下对象的创建过程(半初始化问题)。
假设我有一个类,里面有个成员变量m,值为8
public class T {
int m = 8;
public static void main(String[] args) {
T t = new T();
}
}
请问,这个对象的创建过程是?
idea–>点击view–>Show Bytecode
1. NEW com/zyj/study/algorithm/volatil/T
2. INVOKESPECIAL com/zyj/study/algorithm/volatil/T. ()V
3. ASTORE 1
NEW: 在内存中申请一块空间
当我new这个对象的时候,内存中已经有了一块空间,成员变量m也有了,但是m的值此时是8吗?不是,它的值是int默认的值0,如果类型是boolean,那它的值就是false,如果类型是引用类型,那么它的值是null。
INVOKESPECIAL:调用特殊的方法(T的构造方法)
调用构造方法之后,才会给m赋值,这时候m的值才等于8。
ASTORE 1:建立关联
将t指向已经调用完构造方法的空间。
在对象创建的过程中,成员变量的值是在调用构造方法时才给的,所以说有个半初始化的过程。
那么现在我们结合DCL的代码看下对象的初始化过程。
当一个线程进来的时候,判断对象是否为空?肯定为空,因为还没创建呢,往下执行,拿到锁,继续往下执行,再次判断是否为空?为空,往下执行,在new对象的时候,对象有个半初始化的一个状态,在执行完new
的时候,分配了一块空间,成员变量是引用类型那么它的值为null,就在此时,invokespecial
和astore 1
发生了指令重排序,直接将INSTANCE指向了初始化一半还没有调用构造方法的内存空间,这时候第二个线程进来了,判断对象为空吗?不为空,为啥?因为它指向了一个半初始化的一个对象嘛!既然不为空,我就直接返回了这个初始化一半的对象。
所以说,这段代码要不要加volatile?必须加!加了volatile的这块内存,对于它的读写访问不可以重排序!
那么有的同学会问,为什么是这两条指令发生指令重排序?
什么样的指令可以发生重排序:两条指令互不影响(无关)。
什么样的指令不可以发生重排序:
JVM规定:8种happens-before不可以发生指令重排序,不包括刚才的那两条,所以那两条可以发生重排序
那么volatile是如何禁止指令重排序的呢?敬请期待下篇文章《内存屏障》。