懒汉式

懒汉式:刚开始不初始化,在用的时候再进行初始化。

懒汉单例双重检查真的安全吗?

代码示例:

/**
 * 懒汉式-双重检查-非线程安全
 */
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. 内存中分配空间
    2. 空间初始化
    3. 把这个空间的地址给我们的引用

问题就出现在这这里!!!

有可能它先执行的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();

}