设计模式(5) : 单例模式

定义:

保证一个类仅有一个实例, 并提供一个全局访问点

类型:

创建型

使用场景

  • 确保任何情况下都绝对只有一个实例

coding

单例模式需要注意的点
  1. 私有构造器
  2. 线程安全
  3. 延迟加载
  4. 序列化和反序列化安全
  5. 防止反射机制破坏单例模式

单例模式的N种写法

1. 饿汉式
  • 实现简单
  • 线程安全
public class HungrySingleton {
    private static HungrySingleton INSTANCE = new HungrySingleton();
    private HungrySingleton(){}
    public static HungrySingleton getInstance() {
        return INSTANCE;
    }
}

初始化类时就加载, 如果不使用就会浪费内存

2. 懒汉式
  • 实现简单
  • 延迟加载
public class LazySingleton {
    private static LazySingleton INSTANCE;
    private LazySingleton(){}
    public static LazySingleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new LazySingleton();
        }
        return INSTANCE;
    }
}

懒汉式的优点是延迟加载,等到需要的时候才会创建实例, 但他是线程不安全的, 当两个线程同时进入getInstance方法时, 线程1和2都执行到
INSTANCE == null, 此时INSTANCE如果还未创建, 将会创建两个实例

线程不安全可以通过多线程调试来复现
IDEA多线程调试

3. 懒汉式 + 同步锁
  • 延迟加载
  • 线程安全
public class LazySyncSingleton {
    private static LazySyncSingleton INSTANCE;
    private LazySyncSingleton(){}
    public static synchronized LazySyncSingleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new LazySyncSingleton();
        }
        return INSTANCE;
    }
}

这样子线程就安全了,但是消耗了不必要的同步资源,不推荐这样使用。

4. DCL模式(Double CheckLock) - 双重检查
  • 延迟加载
  • 线程安全
  • 相对懒汉式 + 同步锁的方式只在初始化时才会加锁, 提高了效率
public class LazyDoubleCheckSingleton {
    private static LazyDoubleCheckSingleton INSTANCE;
    private LazyDoubleCheckSingleton(){}
    public static synchronized LazyDoubleCheckSingleton getInstance() {
        if (INSTANCE == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new LazyDoubleCheckSingleton();
                }
            }
        }
        return INSTANCE;
    }
}

通过两个判断,第一层是避免不必要的同步,第二层判断是否为null。
可能会出现DCL模式失效的情况。

DCL模式失效:
singleton=new Singleton()这句话执行的时候,会进行下列三个过程:

  1. 分配内存。
  2. 初始化构造函数和成员变量。
  3. 将对象指向分配的空间。

由于JMM(Java Memory Model)的规定,可能会对单线程情况下不影响程序运行结果的指令进行重排序, 因此可能会出现1-2-3和1-3-2两种情况。
所以,就会出现线程A进行到1-3时,就被线程B取走,此时B线程拿到的是一个还未初始化完成的对象, 这时就出现了异常, DCL模式就失效了。

可以使用 volatile 来解决重排序问题

volatile 有禁止指令重排序的功能. volatile详解

private volatile static LazyDoubleCheckSingleton INSTANCE;
5.内部类实现单例
  • 线程安全
  • 实现简单
  • 延迟加载
public class StaticInnerClassSingleton {
    private static class SingletonHolder {
        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }
    private StaticInnerClassSingleton(){}
    public static StaticInnerClassSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

利用了class的初始化锁保证只有一个线程能加载内部类
只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance (只有拿到初始化锁的线程才会初始化对象)

6.枚举
  • 实现简单
  • 线程安全
  • 避免反序列化破坏单例
  • 避免反射攻击
public enum EnumSingleton {
    INSTANCE(new Object());
    EnumSingleton(Object data) {
        this.data = data;
    }
    /**
     * 单例实体
     */
    private Object data;
    public Object getData() {
        return data;
    }
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

Spring管理单例bean就是容器管理

7.容器
  • 统一管理单例
public class ContainerSingleton {
    private ContainerSingleton(){}
    private static Map CONTAINER = new HashMap();
    public static void putInstance(String key,Object instance){
        if(StringUtils.isNotBlank(key) && instance != null){
            if(!CONTAINER.containsKey(key)){
                CONTAINER.put(key,instance);
            }
        }
    }
    public static Object getInstance(String key){
        return CONTAINER.get(key);
    }
}
8.特殊的单例模式 ThreadLocal 实现线程单例
public class ThreadLocalInstance {
    private static final ThreadLocal INSTANCE
            = ThreadLocal.withInitial(ThreadLocalInstance::new);
    private ThreadLocalInstance(){
        System.out.println("init");
    }
    public static ThreadLocalInstance getInstance(){
        return INSTANCE.get();
    }
}

测试

public class ThreadLocalInstance {
    private static final ThreadLocal INSTANCE
            = ThreadLocal.withInitial(ThreadLocalInstance::new);
    private ThreadLocalInstance(){
        System.out.println("init");
    }
    public static ThreadLocalInstance getInstance(){
        return INSTANCE.get();
    }
}

运行, 在输出结果中可以看到, 用一个线程获取到的实例都是相同的, 即每个线程中只有一个实例存在, 在很多情况下是非常有用的,篇幅原因就不详细展开了,想详细了解的可以看一下这边文章 => Java并发编程:深入剖析ThreadLocal

源码中的单例

单例在源码中是广泛使用的
比如常用的工具类 java.lang.Math#random方法

public static double random() {
        return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();
    }

这个RandomNumberGeneratorHolder.randomNumberGenerator是什么呢?

   private static final class RandomNumberGeneratorHolder {
        static final Random randomNumberGenerator = new Random();
    }

这正是上面提到的内部类实现单例的模式.
其他的比如java.lang.Runtime

    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

一个非常明显的饿汉式单例

序列化对单例模式的破坏

java中提供了对象的序列化与反序列化功能, 对象实现了Serializable接口之后就可以对对象的实例进行序列化与反序列化, 下面以HungrySingleton 为例看一下 反序列化破坏单例模式的实例

public class SerializationBrokenSingletonTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        HungrySingleton instance = HungrySingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
        // 序列化
        oos.writeObject(instance);
        File file = new File("singleton_file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        // 反序列化
        HungrySingleton newInstance = (HungrySingleton) ois.readObject();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

运行结果:

com.hhx.design.pattern.creational.singleton.HungrySingleton@135fbaa4
com.hhx.design.pattern.creational.singleton.HungrySingleton@568db2f2
false

明显看到 反序列化之后, 得到了一个不同的对象实例.

在HungrySingleton中添加readResolve()方法

    private Object readResolve(){
        return hungrySingleton;
    }

再次运行代码

com.hhx.design.pattern.creational.singleton.HungrySingleton@135fbaa4
com.hhx.design.pattern.creational.singleton.HungrySingleton@135fbaa4
true

神奇的发现返回true, 这是怎么回事呢,
有兴趣的朋友可以debug跟踪一下

  1. ObjectInputStream#readObject
  2. ObjectInputStream#readObject0
  3. ObjectInputStream#readOrdinaryObject

重点关注ObjectInputStream#readOrdinaryObject中的
obj = desc.isInstantiable() ? desc.newInstance() : null

Object rep = desc.invokeReadResolve(obj)
就可以知道 readResolve 的调用原理了.

反射对单例模式的破坏

public class ReflectBrokenSingletonTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        HungrySingleton instance = HungrySingleton.getInstance();
        Class clazz = HungrySingleton.class;
        Constructor constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        HungrySingleton newInstance = constructor.newInstance();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}
  • 对于在类加载阶段就初始化的实例的单例模式(饿汉式, 内部类)可以通过在构造器中抛出异常的方式防止反射攻击
    private HungrySingleton(){
        if(INSTANCE != null){
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    }

对于懒加载的单例模式(懒汉式, 懒汉式+同步锁, DCL模式), 如果在构造器中抛出异常的话, 当实例在反射调用constructor.newInstance()执行之前就已经实例化时, 是可以按照预期抛出异常的, 但是如果单例模式中的实例还未被实例化, 执行constructor.newInstance()不会抛出异常, 因为此时INSTANCE == null.

优点:

  • 内存只有一个实例, 减少内存开销
  • 设置全局访问点, 严格控制访问

缺点:

  • 没有接口, 扩展困难

github源码

你可能感兴趣的:(设计模式(5) : 单例模式)