Java设计模式系列:
- 设计模式入门:https://blog.youkuaiyun.com/u011863006/article/details/89223282
- 单例模式:https://blog.youkuaiyun.com/u011863006/article/details/84201592
Java各种技术、各种框架更新的速度越来越快,学习成本越来越高,但是我们学习Java要学习其中不变的部分,其中设计模式就是最高层次的抽象,是高出框架、语言的。所以学习的收益也是最高的,不会被时代淘汰,并且几乎在任何一个面试中都会被问到。
最近在看《Head First设计模式》这本书,准备将其中的感悟结合平时的积累总结一下,写一个设计模式系列博客。
首先就从最简单的单例模式开始吧。单例模式的定义就是确保一个类只有一个实例,并提供一个全局的访问点。那么什么时候需要单例模式呢,比如说:线程池、连接池、缓存、注册表、日志对象,还有打印机、显卡的驱动程序,这些类只能有一个实例,如果有多个实例就对造成混乱。
单例模式从对象生成的时间上可以分为懒汉模式和饿汉模式,懒汉模式比较懒:就是用到这个对象的时候就创建;饿汉模式比较饥渴:就是在类加载的时候就对对象进行创建。饿汉模式比较简单,我们先说饿汉模式。
饿汉模式
我们以一个打印机为例,因为我们只有一个打印机,所有我们对应的程序中只能有一个打印机的实例对象。为了不让程序随便的new出很多对象,我们最想想到的是将构造函数变成私有的。代码如下图:
package com.sheliming.singlenton.hungry;
/**
* 打印机类
* 饿汉模式
*/
public class Printer {
private static Printer printer = new Printer();
private Printer() {
}
public static Printer getInstance() {
return printer;
}
}
这段代码看似很完美,它的好处是只在类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题。它的缺点也很明显,即使这个单例没有用到也会被创建,而且在类加载之后就被创建,内存就被浪费了。
所以下面就进入到我们懒汉式的单例模式:
懒汉式
懒汉式比较复杂,我们一一道来:
1.入门级
懒汉式就是使用的时候new对象,那么就应该在getInstance的时候创建对象,代码如下:
package com.sheliming.singlenton.lazy;
/**
* 打印机类
* 懒汉模式
*/
public class Printer {
private static Printer printer = null;
private Printer() {
}
public static Printer getInstance() {
if (printer == null) {
printer = new Printer();
}
return printer;
}
}
这段代码在单线程的时候没有任何问题,但是到了多线程中,就会出问题。例如:当两个线程同时运行到判断if (printer == null)语句,并且instance确实没有创建好时,那么两个线程都会创建一个实例。
2.加snychronized关键字
既然多线程下有问题,我们首先想到的是在getInstance方法上加上snychronized关键字,这样在多线程的方法中同时只有一个线程可以访问这个方法,这样对象只会被初始化一次,在访问的时候if (printer == null)已经不为null了。代码如下
package com.sheliming.singlenton.lazy;
/**
* 打印机类
* 懒汉模式,带synchronized关键字
*/
public class Printer2 {
private static Printer2 printer = null;
private Printer2() {
}
public synchronized static Printer2 getInstance() {
if (printer == null) {
printer = new Printer2();
}
return printer;
}
}
但是这个方法也有缺点:每次通过getInstance方法得到singleton实例的时候都有一个试图去获取同步锁的过程。而众所周知,加锁是很耗时的。能避免则避免。
3.双重校验锁(Double-Check)
下面这种方法不仅可以避免线程安全的问题,而且可以避免每次获取对象的时候进行加锁,代码如下:
package com.sheliming.singlenton.lazy;
/**
* 打印机类
* 懒汉模式,双重校验锁
*/
public class Printer3 {
private static Printer3 printer = null;
private Printer3() {
}
public static Printer3 getInstance() {
if (printer == null) {
synchronized (Printer3.class) {
if (printer == null) {
printer = new Printer3();
}
}
}
return printer;
}
}
4.终极版本volatile关键字
我们看到双重校验锁即实现了延迟加载,又解决了线程并发问题,同时还解决了执行效率问题,是否真的就万无一失了呢?
这里要提到Java中的指令重排优化。所谓指令重排优化是指在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。JVM中并没有规定编译器优化相关的内容,也就是说JVM可以自由的进行指令重排序的优化。
这个问题的关键就在于由于指令重排优化的存在,导致初始化Singleton和将对象地址赋给instance字段的顺序是不确定的。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错。
以上就是双重校验锁会失效的原因,不过还好在JDK1.5及之后版本增加了volatile关键字。volatile的一个语义是禁止指令重排序优化,也就保证了instance变量被赋值的时候对象已经是初始化过的,从而避免了上面说到的问题。代码如下:
package com.sheliming.singlenton.lazy;
/**
* 打印机类
* 懒汉模式,双重校验锁
*/
public class Printer4 {
private static volatile Printer4 printer = null;
private Printer4() {
}
public static Printer4 getInstance() {
if (printer == null) {
synchronized (Printer4.class) {
if (printer == null) {
printer = new Printer4();
}
}
}
return printer;
}
}
这种方法是可以在生产环境中使用的!!
5、静态内部类
除了以上几种方法,还有2中比较巧妙的方法。首先是静态内部类的方法:
package com.sheliming.singlenton.lazy.staticclass;
/**
* 打印机类
* 静态内部类
*/
public class Printer {
private static class PrinterHolder{
public static Printer printer = new Printer();
}
private Printer() {
}
public static Printer getInstance() {
return PrinterHolder.printer;
}
}
这种写法非常巧妙:
- 对于内部类SingletonHolder,它是一个饿汉式的单例实现,在SingletonHolder初始化的时候会由ClassLoader来保证同步,使INSTANCE是一个真·单例。
- 同时,由于SingletonHolder是一个内部类,只在外部类的Singleton的getInstance()中被使用,所以它被加载的时机也就是在getInstance()方法第一次被调用的时候。
——它利用了ClassLoader来保证了同步,同时又能让开发者控制类加载的时机。从内部看是一个饿汉式的单例,但是从外部看来,又的确是懒汉式的实现。
6.枚举实现
还有最后一种方式:
public enum SingleInstance {
INSTANCE;
public void fun1() {
// do something
}
}
// 使用
SingleInstance.INSTANCE.fun1();
上面提到的四种实现单例的方式都有共同的缺点:
- 需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例。
- 可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。
而枚举类很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,《Effective Java》作者推荐使用的方法。不过,在实际工作中,很少看见有人这么写。但是它仍然不是完美的——比如,在需要继承的场景,它就不适用了。
总结
说了这么多,总结一下生产中经常使用的是:
- 加voletile关键字的双重检查锁。
- 饿汉模式。
参考文献
《Head First设计模式》
https://blog.youkuaiyun.com/goodlixueyong/article/details/51935526
https://www.cnblogs.com/dongyu666/p/6971783.html