懒汉式、饿汉式
懒汉式
懒汉式:刚开始不初始化,在用的时候再进行初始化。
懒汉单例双重检查真的安全吗?
代码示例:
/** * 懒汉式-双重检查-非线程安全 */ public class SingleDclNotsafe { private static SingleDclNotsafe singleDcl; //私有化 private SingleDclNotsafe(){ } public static SingleDclNotsafe getInstance(){ if (singleDcl == null){ //第一次检查,不加锁 System.out.println(Thread.currentThread()+" is null"); synchronized(SingleDclNotsafe.class){ //加锁 if (singleDcl == null){ //第二次检查,加锁情况下 System.out.println(Thread.currentThread()+" is null"); singleDcl = new SingleDclNotsafe(); } } } return singleDcl; } }
为什么是不安全的?
首先,先清楚在 new SingleDclNotsafe();这一步操作上所做的三件事情:
- 内存中分配空间
- 空间初始化
- 把这个空间的地址给我们的引用
问题就出现在这这里!!!
有可能它先执行的1 > 3 > 2,其中在执行完 3 后,第 2 步 还没有完成,这时,这个对象已经不为空,然后直接就返回出去了。。。
就会有一个不完整的对象,怎么说呢?
代码举例:
public class SingleDclNotsafe { private int a; private String s; private static SingleDclNotsafe singleDcl; //私有化 private SingleDclNotsafe(){ } public static SingleDclNotsafe getInstance(){ if (singleDcl == null){ //第一次检查,不加锁 System.out.println(Thread.currentThread()+" is null"); synchronized(SingleDclNotsafe.class){ //加锁 if (singleDcl == null){ //第二次检查,加锁情况下 System.out.println(Thread.currentThread()+" is null"); //内存中分配空间 1 //空间初始化 2 //把这个空间的地址给我们的引用 3 singleDcl = new SingleDclNotsafe(); } } } return singleDcl; } }
在空间地址给这个引用时,可能还未完成空间的初始化,这个时候可能 int a 已经初始化完成了,但是String s 还未初始化就被返回了。这样的话在外部用这个对象 get 属性时:getA()没有问题,getS()就会NullPointException,因为 String s 还未初始化完就已经被返回了。
所以说,它是线程不安全的。
怎么样做到懒汉单例线程安全?
解决方法一:加 volatile 关键字。
通过使用包含多个状态变量的容器对象来维持不变性条件,并且使用一个 volatile 类型的引用来确保可见性,从而保证了线程安全。
volatile:1.可见性;2.避免重排序,JSR屏障
JSR内存屏障:JSR是JavaSpecification Requests的缩写,意思是“Java 规范提案”
LoadLoad:对于这样的语句Load1;LoadLoad;Load2,在Load2及后续的读操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕;
StoreStore:对于这样的语句Store1;StoreStore;Store2,在Store2及后续的写操作执行前,保证Store1的写入操作对其他处理器可见;
LoadStore:对于这样的语句Load1;LoadStore;Store2,在Store2及后续的写入操作被刷出前,保证Load1要读取的数据被读取完毕;
StoreLoad:对于这样的语句Store1;StoreLoad;Load2,在Load2及后续的读操作要读取的数据被访问前,保证Store1的写入操作对其他处理器可见;
as-if-serial:不管如何重排序,单线程执行结果不会改变。在JVM层面volatile的实现细节特别保守,保证了内存可见性,并且成功防止指令重排序。
代码示例:
/** * 懒汉式-双重检查(DCL:double check lock)--线程安全 */ public class SingleDclSafe {
// volatile:1.可见性;2.避免重排序,JSR屏障 private volatile static SingleDclSafe singleDcl; //私有化 private SingleDclSafe() { } public static SingleDclSafe getInstance() { if (singleDcl == null) { //第一次检查,不加锁 System.out.println(Thread.currentThread() + " is null"); synchronized (SingleDclSafe.class) { //加锁 if (singleDcl == null) { //第二次检查,加锁情况下 System.out.println(Thread.currentThread() + " is null"); singleDcl = new SingleDclSafe(); } } } return singleDcl; } }
解决方法二:延迟初始化占位类模式
原理:
运用虚拟机的类加载保证线程安全,虚拟机在实现的时候考虑到:这个类在加载的时候 ,可能跟会有多个线程同时加载这个类,但是在虚拟机里面,某个类只能被加载一次(class对象只有一个)。所以当多个线程同时加载这个类时,虚拟机就会进行加锁,保证只有一个线程完成这个所谓的类加载。
/** * 懒汉式-延迟初始化占位类模式--线程安全 */ public class SingleInit { private SingleInit() { } private static class InstanceHolder { private static SingleInit instance = new SingleInit(); } public static SingleInit getInstance() { return InstanceHolder.instance; } }
饿汉式
饿汉式:在开始就初始化完成了。
饿汉式是线程安全的。
原理:同上面的虚拟机加载原理一样。
代码示例
/** * 饿汉式--线程安全 */ public class SingleEHan { private SingleEHan() { } private static SingleEHan singleDcl = new SingleEHan(); }