学习设计模式之单例模式
单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
类结构图
单例模式有两种实现方式:恶汉式、懒汉式。
饿汉式代码示例
public class Singleton {
private static final Singleton SINGLETON = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return SINGLETON;
}
}
这样写在单线程或多线程下没有任何问题。但是使用资源效率不高,可能 getInstance() 永远不会执行到,但执行该类的其他静态方法或者加载了该类(class.forName),那么这个实例仍然初始化。
那么我们需要进行懒加载,懒汉式代码示例
public class Singleton2 {
private static Singleton2 SINGLETON;
private Singleton2() {
}
public static Singleton2 getInstance() {
if (null == SINGLETON) {
SINGLETON = new Singleton2();
}
return Singleton2.SINGLETON;
}
}
这样写乍一看上去是没有任何问题,先判断是否为 null 为 null 则 new 一个对象,不为 null 则直接返回。在不考虑并发访问的情况下,上述示例是没有问题的。但是在并发的情况下可能返回多个实例问题。
我们来假设有两个线程并发的访问 getInstance() 函数,第一个调用 getInstance 方法的线程A,在判断完singleton 是 null 的时候,线程 A 就进入了 if 块准备创造实例,但是同时另外一个线程 B 在线程 A 还未创造出实例之前,就又进行了 singleton 是否为 null 的判断,这时 singleton 依然为 null,所以线程 B 也会进入 if 块去创造实例,这时问题就出来了,有两个线程都进入了 if 块去创造实例,结果就造成单例模式并非单例。
那么第三种实现方式来了代码示例
public class Singleton3 {
private static Singleton3 SINGLETON;
private Singleton3() {
}
public synchronized static Singleton3 getInstance() {
if (null == SINGLETON) {
SINGLETON = new Singleton3();
}
return Singleton3.SINGLETON;
}
}
这样的做法很简单,就在整个方法上加了 synchronized 来进行方法同步。这样在一个线程访问这个方法时,其它所有的线程都要处于挂起等待状态,倒是避免了刚才同步访问创造出多个实例的危险。但是这样的设计实在是糟糕所以不推荐。
所以有产生了第四种实现方式 Double Check 代码示例
public class Singleton4 {
private static Singleton4 SINGLETON;
private Singleton4() {
}
public static Singleton4 getInstance() {
if (null == SINGLETON) {
synchronized (SINGLETON) {
if (null == SINGLETON) {
SINGLETON = new Singleton4();
}
}
}
return Singleton4.SINGLETON;
}
}
这种做法与上面那种最无脑的同步做法相比就要好很多了,我们不用每次都让线程加锁而只是在实例未被初始化的时候再加锁处理,同时也保证了线程安全。
这样有人会问为什么要加两次 null == SINGLETON 的判断呢,我们仔细来想一想,如果两个线程满足了同步块外的 if 判断,假设 A 线程拿到了锁执行了初始化此时 SINGLETON 已经被赋予了实例。A 线程退出同步块,直接返回了第一个创造的实例,此时 B 线程获得线程锁,也进入同步块,此时 A 线程其实已经创造好了实例,B 线程正常情况应该直接返回的,但是因为同步块里没有判断是否为 null,直接就是一条创建实例的语句,所以 B 线程也会创造一个实例返回,此时就造成创造了多个实例的情况。
那么经过分析后双重校验的代码真的就没有问题了吗?答案并不是,其实仍然还是有问题的。如果我们深入 JVM 中去探索上面的代码,它就有可能(注意,只是有可能)是有问题的。
那么我们来看看 JVM 在创建新对象时主要经过三步:
- 分配内存并初始化零值。
- 初始化构造器。
- 将对象指向分配的内存的地址。
按这样的顺序执行是没有问题的,但是 JVM 为了效率,会自作多情的做了字节码调优,也就是指令重排。他的宗旨简单来说就是保证结果的正确性,但是中间执行顺序就不可保证。如果第二步和第三步顺序颠倒一下,那么很有可能就返回一个还没有初始化好的对象。因为这时将会先将内存地址赋给对象,针对上述的双重加锁,就是说先将分配好的内存地址指给 SINGLETON,然后再进行初始化构造器,这时候后面的线程去请求 getInstance 方法时,会认为 SINGLETON 对象已经实例化了,直接返回一个引用。如果在初始化构造器之前,这个线程使用了 SINGLETON,就会产生莫名的错误。
那么解决办法还是有的,就是在 private static volatile Singleton4 SINGLETON; 在 static 后面增加一个 volatile 就可以了。volatile 有人会问是什么作用呢为什么加上它就可以了,这个我们设计模式之后会介绍!因为不是三言两句就能解释清楚的请各位客官莫着急。
那么这样实现的单例就是最完美的了吗?当然不是,下面我们介绍两种使用最频繁也是最推荐的单例实现。
一、使用静态内部类实现
public class Singleton5 {
private Singleton5() {
}
public static Singleton5 getInstance() {
return InnerSingleton.SINGLETON;
}
private static class InnerSingleton {
private static final Singleton5 SINGLETON = new Singleton5();
}
}
优点:线程安全,调用效率高,可以延时加载。
二、枚举类实现
public enum Singleton6 {
SINGLETON;
// 其它方法
public void doSomething() {
}
}
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊,不过,个人认为由于 1.5 中才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,我也很少看见有人这么写过。
下面让我们来谈一谈如果有人恶意使用反射和序列化反序列化的方式来破坏单例怎么办?
一、反射前面的多种实现方法中,很多我们按照构造方法私有化的思想来实现的,我们知道,利用反射,仍然可以创建出新对象,这样在反射场景中,这种思想实现的单例模式就失效了,那么如何防止反射破坏单例模式呢?原理上就是在存在一个实例的情况下,再次调用构造方法时,抛出异常。下面以饿汉式单例模式为例:
public class Singleton {
private static boolean flag = true;
private static Singleton SINGLETON;
private Singleton() {
if (flag) {
flag = false;
} else {
throw new IllegalStateException();
}
}
public synchronized static Singleton getInstance() {
if (null == SINGLETON) {
SINGLETON = new Singleton();
}
return SINGLETON;
}
}
我们来测试一下:
public static void main(String[] args)
throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
Class objClass = Singleton.class;
Constructor constructor = objClass.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton s1 = Singleton.getInstance();
System.out.println(s1);
Singleton s2 = (Singleton) constructor.newInstance();
Singleton s3 = (Singleton) constructor.newInstance();
System.out.println(s2);
System.out.println(s3);
}
运行结果
com.feil.design.patterns.singleton.Singleton@74a14482
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:408)
at com.feil.design.patterns.singleton.Singleton.main(Singleton.java:40)
Caused by: java.lang.IllegalStateException
at com.feil.design.patterns.singleton.Singleton.<init>(Singleton.java:21)
... 5 more
Process finished with exit code 1
可以看到只有第一次会被创建。
二、序列化反序列化破坏单例模式通过序列化可以讲一个对象实例写入到磁盘中,通过反序列化再读取回来的时候,即便构造方法是私有的,也依然可以通过特殊的途径,创建出一个新的实例,相当于调用了该类的构造函数。要避免这个问题,我们需要在代码中加入如下方法,让其在反序列化过程中执行 readResolve 方法下面以饿汉式单例模式为例:
public class Singleton {
private static boolean flag = true;
private static Singleton SINGLETON;
private Singleton() {
if (flag) {
flag = false;
} else {
throw new IllegalStateException();
}
}
public synchronized static Singleton getInstance() {
if (null == SINGLETON) {
SINGLETON = new Singleton();
}
return SINGLETON;
}
private Object readResolve() throws ObjectStreamException {
return SINGLETON;
}
}
测试:
public static void main(String[] args) throws IOException, ClassNotFoundException {
Singleton s1 = Singleton.getInstance();
System.out.println(s1);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Z:\\Singleton.txt"));
oos.writeObject(s1);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Z:\\Singleton.txt"));
Singleton s2 = (Singleton) ois.readObject();
System.out.println(s2);
}
运行结果
com.feil.design.patterns.singleton.Singleton@74a14482
com.feil.design.patterns.singleton.Singleton@74a14482
Process finished with exit code 0
这个方法是基于回调的,反序列化时,如果定义了 readResolve() 则直接返回此方法指定的对象,而不需要在创建新的对象!
本次单例模式的分享就到此结束了,感谢各位的收看。