单例模式( Single Pattern ): 不仅仅是回字的四种写法

单例模式作为入门编程人员面试必考题之一,也是被玩坏了, 猛然一搜尽然有七种写法,什么懒汉,饿汉五花八门, 这里参考已经比较不错的文章, 忽略五花八门的命名, 把单例模式不同写法按逻辑演进梳理一下, 方便记忆。

参考文章:
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 修饰符多了volatile 关键字。
      • 该关键字保证了一个线程成功实例化instance 后, 该变化立刻对所有的线程可见。 具体细节可以单独查阅volatile 关键字的功用。

上述的单例模式基本上已经可以算是最优的写法了, 下面还有一种利用静态内部类实现的写法, 根本不使用同步机制,与该写法不分伯仲

单例模式线程安全高效写法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.

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