设计模式读书笔记(二)单例模式

2017-04-09  本文已影响10人  learner222

1. 单例模式的实现方式

1.1. 饿汉模式

// 最简单的单例实现
public class Singleton {

    private static final Singleton instance = new Singleton();

    private Singleton() {

    }

    public static Singleton getInstance() {
        return instance;
    }
}
// 饿汉模式的一种变种
public class Singleton {

    private static Singleton instance;

    static {
        instance = new Singleton();
    }

    private Singleton() {

    }

    public static Singleton getInstance() {
        return instance;
    }
}

上面两种实现方式的思想其实是一样的,就是在类加载的时候实例化一个对象,这样避免了线程安全的问题(关于线程安全问题在下面的例子中会提到),但是这也可能会造成一些不必要的资源消耗,比如仅仅载入了类,这时候对象已经实例化了,但是有可能我们永远都用不到它。

1.2. 懒汉模式

public class Singleton {
    
    private static Singleton instance;
    
    private Singleton() {
        
    }

    // 同步方法
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

懒汉模式的整体思想是当需要一个单例对象的时候,判断对象是否已经实例化,如果没有,则进行实例化,否则直接返回已经实例化的对象。这里用了「同步方法」,这样可以保证「线程安全」,当一个线程调用该方法时,另一个方法正好也要调用该方法,则需要等待前一个线程调用完毕,这样就可以保证在多线程的情况下,对象也只会被实例化一次。
但是这种方式在性能上会有一些缺点,因为每次调用 getInstance 方法都需要进行同步,会造成不必要的同步开销。

1.3 Double Check Lock (DCL)模式

public class Singleton {
    
    private static Singleton instance;

    private Singleton() {
        
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

「双重校验锁」模式的重点就是双重校验,它通过两次 instance == null 的判断保证了线程安全,同时避免了上一种「懒汉模式」的同步性能消耗。
我们一步一步进行分析,假设现在有三个线程 A、B 和 C 都要使用单例对象,它们都去调用 getInstance 方法。

  1. 线程 A 先走到外层 if 处,判断发现 instance 还未实例化,此时就会进入同步代码块。
  2. 与此同时,线程 B 也走到了外层 if 语句处,判断发现 instance 仍未实例化,进行下一步,但是由于此时线程 A 已经获得了同步锁,正在执行同步块中的代码,线程 B 需要等待。
  3. 线程 A 进行内层 if 判断,发现 instance 为 null,还没有实例化,则对 instance 进行实例化,然后退出同步块,获得 instance 对象。
  4. 由于线程 A 退出同步块,线程 B 获得同步锁,进入同步块,执行其中的代码,进行内层 if 判断,发现 instance 对象已经被实例化,于是退出同步块,获得 instance 对象。
  5. 线程 C 调用 getInstance,进行外层 if 判断,发现 instance 已经被实例化,直接获得 instance 对象,不再需要进行同步操作。
    通过分析,我们可以得出 instance 两次判空的用意,内层判空是为了在 instance 还未实例化的时候创建实例,而外层判断是为了在 instance 已经实例化的时候避免不必要的同步操作。

然而,这种模式并不是最优的模式,依然存在一些问题,我们继续进行分析。

首先我们需要知道, instance = new Singleton() 这句代码看似是一句代码,但是它最终会被编译成多条汇编指令,大概做了下面 3 件事情:

  1. 为 Singleton 实例分配内存。
  2. 调用构造函数,初始化成员字段。
  3. 将 instance 对象引用指向分配的内存空间,经历这一步之后,instance 才不为 null。
    但是由于 Java 编译器允许处理器乱序处理,也就是说,步骤 2 和步骤 3 的顺序是可以交换的,我们假设一种情况,当一个线程先执行了步骤 1 和 3,但是步骤 2 还没有执行,此时 instance 已经不为 null 了,但成员字段还没有完成初始化,如果此时切换到另一个线程,在外层 if 判空的时候就会得到非空的结果,从而获得成员字段未初始化的 instance 对象,就会导致错误。

除了上面这一点,还有一点就是在 JDK1.5 之前因为 Java 内存模型中的 Cache、寄存器导主内存会写顺序的规定,也会导致这种 instance 非 null,但是成员字段未正确初始化的情况。

因此从 JDK1.5 开始,官方调整了 JVM,新增一个 volatile 关键词,只要在 instance 的定义前加上 volatile 关键字,也就是修改为 private volatile static Singleton instance; 就可以保证不会出现上述问题。

1.4 静态内部类模式

public class Singleton {

    private Singleton() {
        
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
    
    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }
}

这种方式是比较推荐的单例模式的实现方式。这种方式其实是对饿汉式的一种升级,它很好地利用了内部类来避免仅仅载入类,对象就已经实例化的问题。只有在真正需要 instance 对象,调用 getInstance 方法的时候,才会实例化对象。

1.5 枚举单例

public enum SingletonEnum {
    INSTANCE;

    private int i;
    
    public void fun() {

    }
}

最简单的单例模式实现方式就是使用枚举了,其中的 INSTANCE 并没有实际用途,是因为必须要有一个枚举项。枚举在 Java 中和普通类是一样的,可以有成员字段,也可以有成员方法,而且使用枚举不需要写任何额外的代码来保证线程安全。

之前提到的单例模式实现方法在一种特殊情况下仍会重复实例化对象,那就是在反序列化操作的时候,为了避免出现这种情况,我们还需要在代码中加入下面方法:

private Object readResolve()  throws ObjectStreamException {
    return instance;
}

而通过枚举实现,这些都不需要考虑,即使是反序列化也不会重复实例化对象。

1.6 使用容器

public class SingletonManager {

    private static Map<String, Object> objMap = new HashMap<>();

    private SingletonManager() {
        
    }

    public static void registerService(String key, Object instance) {
        if (!objMap.containsKey(key)) {
            objMap.put(key, instance);
        }
    }

    public static Object getService(String key) {
        return objMap.get(key);
    }
}

使用容器来统一管理单例。

上一篇 下一篇

猜你喜欢

热点阅读