一、用双重检查锁定来创建单例,它真的是安全的吗?(懒汉)
单例模式就是我在一个应用程序中某一个类只有一个单例。将其构造方法私有化,不让外面的调用者调用其构造方法。
双重检查
/**
* 懒汉式-双重检查
* 线程不安全的实现
*/
public class SingleDcl {
private static SingleDcl singleDcl;
//私有化
private SingleDcl(){
}
public static SingleDcl getInstance(){
if (singleDcl == null){ //第一次检查,不加锁
System.out.println(Thread.currentThread()+" is null");
synchronized(SingleDcl.class){ //加锁
if (singleDcl == null){ //第二次检查,加锁情况下
System.out.println(Thread.currentThread()+" is null");
//内存中分配空间 1
//空间初始化 2
//把这个空间的地址给我们的引用 3
//指令的重排序
singleDcl = new SingleDcl();
}
}
}
return singleDcl;
}
}
这样,对外提供一个getInstance()方法,外部调用者即可获取到这个对象的实例。考虑到多线程的场景下,先去检查这个对象有没有被创建出来,如果对象为null,则对这个类进行加锁(保证只有一个线程可以进入)。进入这个线程之后,再做第二次检查,如果还没有产生实例,则new出一个对象的实例,将这个实例返回出去。
但是这种实现是一种不安全的实现
加锁之后为什么要做第二次检查?它担心在我进入这个代码块之前,已经有一个线程先进入了,因为CPU切换时间片将线程唤起是需要时间的,检查完如果确实没有创建实例,则证明我是第一个拿到这把锁的,顺其自然地去创建这个对象。
JVM底层
new关键字创建对象的时候,在JVM底层包含了三个动作:
- 1.内存中分配空间,在堆中分配一个内存空间
- 2.空间初始化
- 3.将这个空间的地址给对象的引用
而在jvm底层,有一个指令的重排序。在实际的运行过程中很有可能第二步和第三步交换顺序。
我们的检查if (singleDcl == null)
就是去检查当前的引用有没有指向内存中的地址。一旦发生了指令重排序的话,这个对象的引用指向的就是一个null的空间,由于没有进行初始化,如果当前线程要使用这个对象的成员变量的话,就会报空指针异常。
解决方法?
加一个关键字即可:volatile
在底层,volatile关键字可以抑制指令的重排序。保证线程拿到这个对象的时候一定是一个初始化完成了的。
二、饿汉式
声明这个对象的时候,就将对象初始化好。
/**
* 饿汉式
*
*/
public class SingleEHan {
private SingleEHan(){}
public static SingleEHan singleDcl = new SingleEHan();
}
为什么这种情况线程安全?
因为加入了static关键字,由虚拟机替我们进行加锁,可以保证只有一个线程执行类加载,是由虚拟机的类加载机制保证的。
三、懒汉---延迟初始化占位类模式
/**
* 懒汉式-延迟初始化占位类模式
*/
public class SingleInit {
/**
* 懒汉式-延迟初始化占位类模式
*/
public class SingleInit {
private SingleInit(){}
private static class InstanceHolder{
private static SingleInit instance = new SingleInit();
}
public static SingleInit getInstance(){
return InstanceHolder.instance;
}
}
定义一个静态的内部类,里面有个静态变量去实例化对象。实例化的操作是在这个类的内部有一个内部类去实现的。
网友评论