读《大话设计模式》- 单例模式延伸

159 阅读4分钟

1.单例模式分类

根据实例创建的时机单例模式分为两种,饿汉式和懒汉式。

饿汉式

  • 类加载的时候立即创建单例对象,线程安全(相对安全),不用加任何锁,执行效率比较高
  • 类初始化就创建,不用也会占用内存

懒汉式

  • 被外部调用的时候实例才会创建,需要锁控制线程安全,执行效率相对低。

1.1饿汉式单例

  • 第一种饿汉

    public class HungrySingleton implements Serializable {
        /**
         * 饿汉式单例,没有任何锁.执行效率高
         * 但是内加载的时候就初始化,不用也会创建占用空间浪费内存空间站着空间不拉屎.
         * 线程相对安全的[序列化和反射可破坏]
         *
         * 因为类只加载(初始化)一次,静态变量和代码块都是在初始化的时候赋值 ,所以只可能赋值一次,所以只可能是单例的.
         */
        private final static HungrySingleton INSTANCE = new HungrySingleton();
    ​
        // 私有化构造参数
        private HungrySingleton() {}
    ​
        public static HungrySingleton getInstance() {
            return INSTANCE;
        }
     }
    
  • 第二种饿汉

    public class HungryStaticSingleton {
    ​
        private  static HungryStaticSingleton INSTANCE = null;
    ​
        private HungryStaticSingleton() {}
    ​
        static {
            INSTANCE = new HungryStaticSingleton();
        }
    ​
        public static HungryStaticSingleton getInstance() {
            return INSTANCE;
        }
     }
    
  • 特殊的饿汉[枚举单例]

    public enum EnumSingle {
        INSTANCE;
        public static EnumSingle getInstance() {
            return INSTANCE;
        }
      }
    
  • 饿汉式单例是利用Java类只初始化一次,而静态变量的赋值和静态代码块的执行是在初始化的时候发生的,所以只会有一个实例。同样他也是一种注册式单例,通过容器维护的

image.png

1.2懒汉式单例

  • 普通懒汉

    public class LazySimpleSingleton {
        /**
         * 懒汉式,外部需要的时候才实例化.节省空间
         * 但是因为锁的存在,执行效率低
         */
        private static LazySimpleSingleton INSTANCE = null;
        private LazySimpleSingleton() {}
        
        /**
         * 直接在方法上加锁,不管实例是否已创建都阻塞想要获取实例的线程。
         * 在对象创建后还阻塞,实际上是不必要的降低执行效率,不太OK
         */
        public static synchronized LazySimpleSingleton getInstance() {
    ​
            if (INSTANCE == null) {
                INSTANCE = new LazySimpleSingleton();
            }
            return INSTANCE;
        }
     }
    
  • 双重检查锁懒汉

    public class LazyDoubleCheckSingleton {
    ​
        /**
         * 避免指令重排 .保证变量的可见性
         */
        private volatile static LazyDoubleCheckSingleton INSTANCE = null;
    ​
        private LazyDoubleCheckSingleton() {}
        /**
         * 双重检查锁的意义
         * synchronized不放在方法上缩小锁的范围,只有为null的时候才锁.避免每次都锁上
         * 里面的双重检查是避免两个方法都进入第一个判断了,就会再次生成两个实例
         * 还是因为锁的原因导致不是执行效率不是特别好
         * @return
         */
        public static LazyDoubleCheckSingleton getInstance() {
    ​
            if (null == INSTANCE) {
                synchronized (LazyDoubleCheckSingleton.class) {
                    if (null != INSTANCE) {
                        INSTANCE = new LazyDoubleCheckSingleton();
                        /**
                         * new 的操作不是原子性的.分为3步
                         * 1.申请内存并分配给这个对象
                         * 2.实例化对象
                         * 3.设置对象指向刚分配的内存空间
                         * 2,3步可能重排需要volatile禁止指令重排
                         * A线程132
                         * B线程123 还没构造完成但是已经有了内存空间
                         */
                    }
                }
            }
            return INSTANCE;
        }
     }
    
  • 静态内部类懒汉

    public  class LazyInnerClassSingleton {
    ​
        private LazyInnerClassSingleton() {}
    ​
        public final static LazyInnerClassSingleton getInstance() {
            return LazyHolder.LAZY;
        }
        /**
         * 巧妙的做法,是懒汉式
         * 内部类只有在被使用的时候才会加载进内存执行一次初始化的过程,
         * 也就是静态变量赋值是懒加载的,只有调用才会加载.
         * 又通过静态变量只赋值一次保证了单例
         */
        private static class LazyHolder {
            private final static LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
        }  
      }
    
    • 既避免了内加载的时候就创建实例浪费空间也避免了锁阻塞执行效率

1.3ThreadLocal伪单例

public class ThreadLocalSingleton {
​
    private ThreadLocalSingleton() {}
    /**
     * 利用ThreadLocal 的伪单例,实际上是每个线程保留一份自己的对象.
     * 线程内部单例
     */
    private final static ThreadLocal<ThreadLocalSingleton> ioc = ThreadLocal.withInitial(() -> {
        // 初始化的时候直接设置值
        return new ThreadLocalSingleton();
    });
    public static ThreadLocalSingleton getInstance() {
        return ioc.get();
    }
  }
  • 借助ThreadLocal通过内部类ThreadLocalMap,将其绑定在线程上,key值为ThreadLocal实例保证唯一

1.4 容器式单例

public class ContainerSingleton {
    /**
     * 注册式的单例 ,集中管理.类似springioc容器
     */
    private static final ConcurrentHashMap<String, Object> ioc = new ConcurrentHashMap<>();
​
    public static Object getInstance(Class clazz) {
        String name = clazz.getName();
​
        if (!ioc.containsKey(name)) {
            synchronized (ioc) {
                if (!ioc.containsKey(name)) {
                    try {
                        Class<?> aClass = Class.forName(name);
                        ioc.put(name, aClass.newInstance());
                    } catch (Exception e) {
                        System.out.println("e = " + e);
                    }
                }
            }
        }
        return ioc.get(name);
    }
  }
  • 统一管理单例实例

2.单例的线程安全问题

除了枚举类型的单例其他的都有线程安全问题

2.1反射破坏单例

只要通过反射获取到私有的构造函数即可生成一个新的实例对象,破坏单例。而枚举因为不能被反射所以枚举类型的单例不会被反射破坏

image-20220422181251303.png

2.2 序列化破坏单例

反序列化的时候,Object类型的会判断有不有无参构造desc.inInstantiable有的话直接newInstance生成一个新的实例,破坏单例。详见ObjectInputStream.readOrdinaryObject

image-20220422181601571.png

而枚举类型的因为直接使用类和枚举名确定唯一实例的,所以不会被破坏。详见ObjectInputStream.readEnum

image-20220422181735562.png

解决序列化破坏单例的问题

回到ObjectInputStream.readOrdinaryObject,有个判断 hasReadResolveMethod,如果有readResolve方法即调用方法覆盖返回对象。

image-20220422182201703.png

image-20220422182928284.png

image-20220422182137635.png

image-20220422182123743.png

于是我们在单例类写个 readResolve 方法,返回当前的唯一实例,即可避免序列化对单例的破坏。

3.枚举为什么是饿汉式的?

我们可以使用jad反编译刚刚的枚举单例类,发现他是在静态代码块中实例化对象的,只有一个双参构造,而且他的实例前都加上了final使之不可改变。

枚举反编译.png