【Java EE】-多线程编程(五) 多线程案例之单例模式-LMLPHP

一、饿汉模式

三招:实例私有化、构造方法私有化、只提供getInstance静态方法供外部类使用。

package demo;
// 单例模式
// 饿汉式
class Singleton {
    // 实例私有化
    private static Singleton instance = new Singleton();
    // 构造方法私有化
    private Singleton(){}

    public static Singleton getInstance(){
        return instance;
    }

}
public class Demo1 {
    public static void main(String[] args) {
        // 因为Singleton类构造方法私有,所以不能在Singleton类外new,而只能调用getInstance方法
        // Singleton对象在类加载时就已经创建了,程序员不能再去手动创建,这样就实现了单例,即只有一个实例Singleton对象
        //demo.Singleton s = new demo.Singleton();  // 报错
        Singleton s = Singleton.getInstance();
    }
}

二、懒汉模式

1、具体分析

按照饿汉模式代码的思路,我们写出的懒汉模式的代码如下,但是,下面的代码有问题,什么问题呢?让我们接着往下看

class SingletonLazy {
    private static SingletonLazy instence = null;
    private SingletonLazy(){}

    public static SingletonLazy getInstance(){
        if(instence == null){
            instence = new SingletonLazy();
        }
        return instence;
    }
}

       我们来看看 if(instance == null)这个条件,当多个线程同时进入if,那么这些线程把instance的值拷贝到寄存器上的值都为null,然后寄存器的值和null比较,这个判断就是true,那这些线程都会去创建SingletonLazy对象,那么单例模式就失效了。为什么会创建多个对象呢?其实就是instance的读和写不是原子性导致问题,所以我们就给它加锁,变成如下这样。
【Java EE】-多线程编程(五) 多线程案例之单例模式-LMLPHP

       这个改造后的代码还存在问题,你想一下,每次在其它类里使用SingletonLazy s = SingletonLazy.getInstance();获取单例的时候,如果是上面的代码,每次都会加锁,然后再判断。实际上,我们只需要第一次创建实例的时候加锁就行了,因为除去第一次后就只涉及到读,而不涉及到写,所以就不需要加锁以保证效率问题。所以我们就再在加锁外面给一个判断。
【Java EE】-多线程编程(五) 多线程案例之单例模式-LMLPHP

       代码写到这里你以为没有问题了?不不不,还有问题!!!
我们假设一个可能存在的场景:此时有很多线程,都去执行getInstance,而此时这些线程都执行到了外层的if这里,然后有一个线程可以获取到锁,然后去内部执行。那么问题来了,这个时候如果内部的if(instance == null)被第一个线程使用时,先把instance的值load到寄存器(或者cache)上,然后比较,然后创建单例。但是当第一个线程释放锁,后续的线程获取到锁进入后,如果不从内存把instance的值加载到寄存器,而是直接从寄存器读那后续线程读到的仍是null,那就又会去创建单例,导致单例失效。这是内存可见性导致单例失效(可以看看之前的博客线程安全里面详细讲了内存可见性问题)。
       接下来还有指令重排序可能导致单例失效。我们先来了解:instance = new SingletonLazy 具体的操作可分为三步:1> 申请内存空间 2> 调用构造方法,把这个内存空间初始化为一个合理的对象 3> 把内存空间的引用给instance对象,如果第一次SingletonLazy s = SingletonLazy.getInstance();创建单例对象时,是1、2、3的顺序执行,那么没有问题。那现在我们又来假设一个可能存在的场景:t1和t2线程,第一次执行getInstance创建单例对象时,t1先执行完1、3,然后被切出CPU(此时t1释放了锁),t2来执行。但是 t2看到t1执行完 3后,就认为instance非空了,就直接返回instance,且t2还可能会使用引用中的属性。但是由于t1还没有执行2操作,所以此时t2拿到这个instance是一个不完整对象,那就可能会出问题。
       那我们从上面的两个分析里知道,上面经过改造的代码还可能存在内存可见性和指令重排序问题,那很明显,就是用volatile解决就行了。改造代码如下:
【Java EE】-多线程编程(五) 多线程案例之单例模式-LMLPHP

2、正确代码和总结

懒汉模式除了三招:实例私有化、构造方法私有化、只提供getInstance静态方法供外部类使用外,还要考虑很多问题:
1> synchronized加锁
2> 双层if判断各自作用
3> volatile解决内存可见性和指令重排序问题

package demo;
// 单例模式
// 懒汉模式
class SingletonLazy {
    // volatile 解决内存可见性和指令重排序问题
    private volatile static SingletonLazy instence = null;
    private SingletonLazy(){}

    public static SingletonLazy getInstance(){
        // 外面的这层if用于判断,如没有这一层判断,则每次进来都会加锁,消耗大量时间做无用功
        if(instence == null){
            // synchronized保证instance的读和写是原子性的
            synchronized (Singleton.class){
                // 判断单例是否已经创建,若无,则创建。
                if(instence == null){
                    instence = new SingletonLazy();
                }
            }
        }
        return instence;
    }
}
04-08 00:13