单例模式2-懒汉式

懒汉式,跟饿汉式相反,类加载的时候并不会被实例化,而是在第一次被调用的时候被实例化。

懒汉式单例奥义

  • 构造器私有!!!
  • 暴露getInstance()方法!!!
  • 在第一次被调用的时候进行初始化

写法1(存在线程安全问题喔)

public class LazySimpleSingleton {

    private static LazySimpleSingleton lazy = null;

    /**
     * 构造器私有!
     */
    private LazySimpleSingleton(){}

    /**
     * 对外暴露的实例化接口
     * @return
     */
    public static LazySimpleSingleton getInstance(){
        if(lazy == null){
            lazy = new LazySimpleSingleton();
        }
        return lazy;
    }

}

自己多线程跑一下,打印一下就知道不是一个对象了。

改进1(synchronized搞他)

    public synchronized static LazySimpleSingleton getInstance(){
        if(lazy == null){
            lazy = new LazySimpleSingleton();
        }
        return lazy;
    }

再多线程跑一下,好像解决了一样,其实并不是解决的很好,在更多线程的情况下,会产生大量阻塞,性能会下降。

改进2(双重检查锁)

    /**
     * 双重检查锁
     * @return
     */
    public synchronized static LazySimpleSingleton getInstance3(){
        // 第一重检查
        if(lazy == null){
            // 锁class
            synchronized (LazySimpleSingleton.class){
                // 第二重检查
                if(lazy == null){
                    lazy = new LazySimpleSingleton();
                }
            }
        }
        return lazy;
    }

把锁的范围变小了,只会在多个线程抢着去new才会产生点点的阻塞,所以只要锁的内容不多,调用者感知不强。

  • 第一重检查好说,就是看是不是为null,需不需要进行new
  • 第二重则是因为可能很多个线程同时通过了第一重检查,想象一下去掉第二重会发生什么事情(只要经过第一重,都会new一次),所以需要第二重检查。

隐患

看上去该改进2又单例,性能也不差,然而还隐藏着一个问题:指令重排序
正常new的时候干了什么?

  1. 分配内存空间
  2. 初始化对象
  3. 将对象指向刚分配的内存空间
    然而有的编译器为了提高性能,会将2和3对调:
  4. 分配内存空间
  5. 将对象指向刚分配的内存空间
  6. 初始化对象
    这样就会产生一个巨大的问题,有可能在第一次new的时候,只new到第二步指向内存,还没执行真正的初始化,这时候其他线程会经过第一重检查直接到return,然后拿到还没初始化的对象进行调用。

改进3 (双重检查锁——volatile防止指令重排序)

private volatile static LazySimpleSingleton lazy = null;

对的,就改一句,volatile可以防止指令重排序,所有的写操作都将发生在读操作之前,这是完整的懒汉式双重检查锁单例;

改进4 (静态内部类)

虽然说双重检查锁性能已经不差了,但是能不能再提高性能一点呢?从类初始化的角度考虑:
这里主要是用到了一点:内部类要在方法调用之前被初始化,意味着它能巧妙避开线程安全问题,而且代码简单。

public class LazyInnerClassSingleton {

    /**
     * 构造器私有
     */
    private LazyInnerClassSingleton() {
    }

    public static LazyInnerClassSingleton getInstance() {
        return LazyHolder.LAZY;
    }

    /**
     * 默认不加载该内部类
     */
    private static class LazyHolder {
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }

}

改进5(不法分子用反射破坏单例)

我们知道反射可以破解private关键字,所以能够通过反射调用构造方法,具体如下:

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class clazz = LazyInnerClassSingleton.class;
        Constructor c = clazz.getDeclaredConstructor(null);
        c.setAccessible(true);
        LazyInnerClassSingleton lazy1 = c.newInstance();
        LazyInnerClassSingleton lazy2 = LazyInnerClassSingleton.getInstance();
        System.out.println(lazy1 == lazy2);//false
    }

那解决调用构造器来搞事情的反射也很简单,只要在构造方法加个判断就行:

private LazyInnerClassSingleton() {
    if (LazyHolder.LAZY!=null){
        throw new RuntimeException("不准搞事");
    }
}

重新执行上面的测试代码,可以发现反射搞不了事了。

不算改进(序列化破坏单例)

有时候(很少,我几乎没见过,当做没有)会把对象序列化写到磁盘,到时候拿出来用,这样做如果是单例对象,就破坏单例了。
太长不写...

注册式单例(两种:枚举、容器)

枚举单例模式

public enum SingletonEnum {
    /**
     * 单例
     */
    INSTANCE;
    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public static SingletonEnum getInstance() {
        return INSTANCE;
    }
}

进行测试:

  1. 调用getInstance()方法,把单例对象塞到data里面
  2. 序列化枚举对象
  3. 反序列化枚举对象
  4. 比较是否同一个对象
    结果是同一个对象

为何?
反编译发现INSTANCE被静态代码块初始化了,是一种饿汉式

你可能感兴趣的:(单例模式2-懒汉式)