好久没写博文,最近学习一些设计模式,顺便记录一下。
单实例Singleton设计模式可能是被讨论和使用的最广泛的一个设计模式了,这可能也是面试中问得最多的一个设计模式了。我们尝试从场景出发,来看看要怎么设计这个类。
场景
我们要得到一个类,整个系统中只能出现一个类的实例。这样的场景非常多,比如说一个国家,只有能有一个现任总统。仔细想想,要满足这一条件,我们觉得应该满足几个条件。
- 和大部分类不同,它的构造函数需要是私有的。否则,在任何地方大家都能够new这个实例,那么系统中就不能始终保持只存在一个实例的情况了。
- 既然没有公有构造函数,那么我如何实例化这个类呢?我们需要一个静态的方式让其形成实例,给个方法吧--getInstance(),这个方法要判断现在系统中有没有这个实例,如果有,则返回;如果没有则调用私有的构造方法来实例化这个类,并存好,等下次来getInstance()调用时返回。
- 所以我们还需要一个私有的变量来存储这个实例。
- 我们使用时就可以用**Singleton.getInstance()**得到它了。
根据这些条件我觉得我们可以得到朴素的教科书版本的代码:
基础版本(懒汉式,线程不安全)
看上去很美好,解决了我们上诉的要求,满足懒加载(只有用到的时候才会去创建这个实例)。然而该方法有一个致命的弱点,当系统中几个线程同时调用这个方法时,就很有可能会实例化出多个实例来,也就是说线程不安全。为了解决这个问题最简单的方法就是加Synchronize关键字。代码如下:
懒汉式,线程安全
好了,线程安全了,然而我们发现在每次调用getInstance()的方法时,我们都会上锁。但其实我们只需要在创建的时候上锁,而不创建的时候我们其实不需要上锁。如果在多线程的系统中有频繁的调用,那么这段代码的性能会比较低。那我们只在创建的时候加锁行不行?像这样:
懒汉式,线程不安全
看起来不错哦。应该没有问题了吧?!错!这还是有问题!为什么呢?前面已经说过,如果有多个线程同时通过(singleton== null)的条件检查(因为他们并行运行),虽然我们的synchronized
方法会帮助我们同步所有的线程,让我们并行线程变成串行的一个一个去new,那不还是一样的吗?同样会出现很多实例。线程依然不安全,没有解决第一种方法的问题!!!!好了我知道了,这样,双重校验:
双重检验锁(double checked locking)
但是这种方法要对volatile关键字有想到深刻的理解,并且对Java的内存模型深度理解。同时这种方法在jdk1.5之前是不能有bug的。如果你在面试中使用了这种方法,但是又不能很好的解释这方法的话。面试官不会喜欢你。-,- 相信你不会喜欢这种复杂又隐含问题的方式,如果你仍有兴趣,请查看这里当然我们有更好的实现线程安全的单例模式的办法。
饿汉式 static final field法
这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。
也就是说这种方法巧妙的避开了新建的过程,所以也不存在多线程调用时会产生的问题。大部分的情况下,这种方法能满足要求。但是吹毛求疵一下,这种方法在没用到它的时候它就已经实例化好了,它不是懒加载的形式。
静态内部类 static nested class
老版《Effective Java》中推荐的方法,上面这种方式,仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它只有在getInstance()被调用时才会真正创建;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。
最优雅版本--枚举 Enum
用枚举写单例实在太简单了!这也是它最大的优点。下面这段代码就是声明枚举实例的通常做法。
居然用枚举!!看上去好牛逼,通过EasySingleton.INSTANCE来访问,这比调用getInstance()方法简单多了。
默认枚举实例的创建是线程安全的,所以不需要担心线程安全的问题。但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。
这个版本基本上消除了绝大多数的问题。代码也非常简单,实在无法不用。这也是新版的《Effective Java》中推荐的模式。
总结
小小的一个场景演化出了这么多方法。在一般的情况下饿汉式的方法用的比较多,在有懒加载要求时,静态内部类方法不错。