单例模式作为入门编程人员面试必考题之一,也是被玩坏了, 猛然一搜尽然有七种写法,什么懒汉,饿汉五花八门, 这里参考已经比较不错的文章, 忽略五花八门的命名, 把单例模式不同写法按逻辑演进梳理一下, 方便记忆。
参考文章:
1. 单例模式的八种写法比较
2. Wiki: Initialization-on-demand holder idiom
3. Java Singleton Design Pattern Best Practices with Examples
单例模式的应用场景
- 整个应用中只需要特定类型的实例需要全局唯一, 否则应用程序就没法正常运行。
单例模式的最原始写法(线程不安全)
public class Singleton
{
private static Singleton instance; // can only be accessed by getInstance
private Singleton() // can not called by outside to create more instances
{
...
}
public static Singleton getInstance()
{
if (instance == null)
instance = new Singleton();
return instance;
}
}
单例模式的代码乍一看很简单, 但是对于第一次看到单例模式的初学者来说, 有几个细节需要关注。
- 单例模式的成员变量instance 的修饰符必须是 private static
- 因为静态变量是被所在类的所有实例共享的
- 单例模式的构造函数必须用private修饰
- 从语法层面保证其他类中,根本无法获得实例化该类的权限
- 单例模式获取单例的方法getInstance用public static synchronized 修饰
- public static关键字修饰的方法为其他类获得单例模式的唯一实例提供了接口
上述写法仅仅适用于不会有多个线程同时调用getInstance方法,现假设有两个线程同时调用getInstance, 假设线程A 刚刚执行完if( instance == null )
后,时间片用尽, 线程B也执行到了if( instance == null )的判断, 此时线程B也会通过该判断, 至此之后, 无论CPU如何调度, 线程A和线程B都会执行一次new Instance , 从而导致线程A和线程B获得的对象是不一样的,违背了单例模式创建实例的唯一性。
所以接下来需要解决的是多线程模式下的单例模式
单例模式的线程安全写法
- 通过synchronized关键字保证线程安全(非常简单)
public class Singleton
{
private static Singleton instance; // can only be accessed by getInstance
private Singleton() // can not called by outside to create more instances
{
…
}
public static synchronized Singleton getInstance()
{
if (instance == null)
instance = new Singleton();
return instance;
}
}
该写法和最原始的写法相比, 仅仅多了一个synchronized 关键字, 保证了同一时刻只有一个线程可以进入getInstance方法。
- 缺点:
- 同一时刻只有一个线程可以执行getInstance()方法,实际上, 只要在instance 被初始化了以后, return instance 是可以被并发执行的。
单例模式的线程安全高效写法1
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
- 与上一个的线程安全写法相比,有两个需要注意的变化。
- synchronized关键字仅仅修饰了
if(singleton == null)
以后的内容。
- 这个变动好理解, 因为当instance 被实例化以后, return是可以并发执行的。
- synchronized关键字修饰的块的内部又加了一次重复地判断
if(singleton == null)
- 这个变动需要思考一下, 因为在instance 尚未被初始化时, 还是有可能有多个线程同时通过
if(singleton == null)
的判断。 例如线程A 通过了if(singleton == null)
的判断,进入了synchronized部分, 在synchronized方法执行到一半时被挂起, 线程B得到调度, 此时同样会通过if(singleton == null)
的判断, 虽然无法立即进入synchronized块, 但是等待线程A执行完synchronized部分以后, 线程B还是会再次进入synchronized方法。
- 这个变动需要思考一下, 因为在instance 尚未被初始化时, 还是有可能有多个线程同时通过
- 成员变量的instance 修饰符多了volatile 关键字。
- 该关键字保证了一个线程成功实例化instance 后, 该变化立刻对所有的线程可见。 具体细节可以单独查阅volatile 关键字的功用。
- synchronized关键字仅仅修饰了
上述的单例模式基本上已经可以算是最优的写法了, 下面还有一种利用静态内部类实现的写法, 根本不使用同步机制,与该写法不分伯仲
单例模式线程安全高效写法2
public class Singleton {
private Singleton() {}
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
这种方法利用了jvm 的类装载机制来保证线程安全, 因为静态变量的初始化是在类被加载的时候时进行的, 而jvm 加载类时, 是只允许一个线程进入的, 这样就保证了线程安全。 同时, 由于是静态内部类, 所以并不会在Singleton 被加载的时候就初始化LazyHolder, 而是当getInstance() 被调用时才会加载LazyHolder。
总结
以上的三种线程安全写法基本上涵盖了单例模式最重要的知识点,对于工程实践来说, 线程安全高效写法1 和 线程安全高效写法2 掌握一种即可。 但是以上介绍的写法的线程安全其实都可以被反射调用所违背, 如果想避免反射调用违背线程安全, 可以采用枚举方式的线程安全写法, 但是这种考量太不常用了, 也无法实现延迟加载, 有兴趣者可以阅读参考文章3.