6.Java1.5以后安全的DCL版本
(1)实现代码
public class Singleton { private volatile static Singleton instance = null; // 获得对象实例的方法 public static Singleton getSingleton() { if(instance == null) { synchronized(Singleton.class) { if(instance == null) instance = new Singleton(); } } return instance; } private Singleton() { } } |
(2)原理分析
可以发现代码只做一点小的修改(把instance声明为volatile型),为什么volatile可以解决呢?回顾一下他的两层语义:
(1)可见性:指的是在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory)
(2)禁止指令重排序优化
当声明对象的引用为volatile后,“问题的根源”的三行伪代码中的2和3之间的重排序,在多线程环境中将会被禁止,从而在根本上解决了问题。但是很不幸,禁止指令重排优化这条语义直到jdk1.5以后才能正确工作。此前的JDK中即使将变量声明为volatile也无法完全避免重排序所导致的问题。所以,在jdk1.5版本前,双重检查锁形式的单例模式是无法保证线程安全的。
写到这里,可能有的同学会有疑问了:
7.Java1.4以前安全的DCL版本
额(⊙o⊙)…虽然现在的日常开发已经普遍在使用Java1.7甚至1.8了。不过尝试着探讨下在Jav1.4以前实现安全的DCL还是一个还有意思的话题。我自己也没有找到太确定的答案,这方面的资料也非常的少。下面给出的实现代码不一定能保证正确,贴出来仅供参考,欢迎有兴趣的同学在评论区留言分享一下经验。
(2)实现代码及思路
public class Singleton { private static Singleton instance = null; // 获得对象实例的方法 public static Singleton getSingleton() { if(instance == null) { //1.第一次检查 synchronized(Singleton.class) { //2.第一个synchronized块 Singleton temp = instance; //3.给临时变量temp赋值 if(temp == null) { //4.第二次检查 synchronized(Singleton.class) { //5.第二个synchronized块 temp = new Singleton(); //6.解决问题的关键地方 } instance = temp; //7.把temp的引用赋值给instance } } } return instance; } private Singleton() { } } |
上面给出的代码中,很关键的地方在于在synchronized块中引入了一个临时变量Singleton temp,通过对temp的判空及相应的初始化,保证在代码7处,执行intance = temp;时,instance不为null且完成了初始化。
8.内部类方式
(1)实现
在第五部分的结尾我们提到了两个办法来实现线程安全的延迟初始化,内部类方式正是基于第二种方法--线程之间重排序透明性
实现代码为:
public class Singleton { // 获得对象实例的方法 public static Singleton getSingleton() { return SingletonHolder.instance; } /** * 静态内部类与外部类的实例没有绑定关系,而且只有被调用时才会 * 加载,从而实现了延迟加载 */ private static class SingletonHolder { /** * 静态初始化器,由JVM来保证线程安全 */ private static Singleton instance = new Singleton(); } private Singleton() { } } |
(2)原理分析
在这种方式中,使用了一个专门的内部类来初始化Singleton,JVM将推迟SingletonHolder的初始化操作,直到开始使用这个类时才初始化。并且在初始化的过程中JVM会去获取一个用于同步多个线程对同一个类进行初始化的锁,这样就不需要额外的同步。这种方式不仅能够保证线程安全,也能保证单例对象的唯一性,同时也延迟实例化,是一种非常推荐的方式。
9.枚举方式
(1)实现方法
从Java1.5起,可以通过使用枚举机制来实现单例模式:
public enum Singleton {
// 定义枚举元素,他就是Singleton的一个实例
INSTANCE;
public void doSomething() {
// do something
}
}
调用方式
Singleton singleton = Singleton.INSTANCE;
singleton.doSomething();
可以看到实现的代码非常的简洁,按照Joshua Bloch大神的原话来说:
While this approach has yet to be widely adopted,a single-element enum type is the best way to implement a singleton.
(2)序列化与反序列化的问题
在上述的几种单例模式实现中,在一个情况下它们会出现重新创建对象的情况,那就是反序列化。
通过序列化可以将一个单例的实例对象写到磁盘,然后再读回来,从而有效地获得一个实例。即使构造函数是私有的,反序列化时依然可以通过特殊的途径去创建类的一个新的实例,相当于调用该类的构造函数。反序列化操作提供一个很特别的钩子函数,类中具有一个私有的、被实例化的方法readResolve(),这个方法可以让开发人员控制对象的反序列化。例如,上述几个实例中如果要杜绝单例对象在被反序列化时重新生成对象,那么必须加入如下方法:
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}
也就是在readResolve方法中将实例对象返回,而不是默认的重新生成一个新的对象。
(3)Java反射攻击
下面我们基于内部类实现的单例模式的方式,来演示一下通过JAVA的反射机制来“攻击”单例模式:
public class TestMain { public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { Class<?> classType = Singleton.class; Constructor<?> c = classType.getDeclaredConstructor(null); c.setAccessible(true); Singleton singleton1 = (Singleton) c.newInstance(); Singleton singleton2 = Singleton.getSingleton(); System.out.println(singleton1 == singleton2); } } |
运行结果:false,可以看到,通过反射获取构造函数,然后调用setAccessible(true)就可以调用私有的构造函数,所有singleton1和singleton2是两个不同的对象。如果要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。
修改原有代码为:
public class Singleton { public static Singleton getSingleton() { return SingletonHolder.instance; } private static class SingletonHolder { private static Singleton instance = new Singleton(); } private static boolean flag = false; private Singleton() { synchronized(Singleton.class) { if(flag == false) { flag = !flag; } else { throw new RuntimeException("单例模式被破坏!"); } } } } |
再次运行上面的测试代码:得到的结果为:
Exception in thread "main" java.lang.RuntimeException: 单例模式被破坏!
at com.danli.Singleton.<init>(Singleton.java:29)
at com.danli.Singleton.getSingleton(Singleton.java:12)
at com.danli.TestMain.main(TestMain.java:23)
可以看到,成功的阻止了单例模式被破坏。
但是我们如果直接基于枚举方式实现的单例模式进行同样的代码测试,会直接得到结果:
Exception in thread "main" java.lang.NoSuchMethodException: com.danli.Singleton.<init>()
at java.lang.Class.getConstructor0(Class.java:2730)
at java.lang.Class.getDeclaredConstructor(Class.java:2004)
at com.danli.TestMain.main(TestMain.java:20)
可以看到,枚举方式实现的单例自己是可以避免反射攻击的。
(4)枚举方式的优点
饿汉式、懒汉式、双重校验锁(DCL)还是静态内部类都存在的缺点:
都需要额外的工作(Serializable、transient、readResolve())来实现序列化,否则每次反序列化一个序列化的对象实例时都会创建一个新的实例。
可能会有人使用反射强行调用我们的私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。
枚举类很好的解决了这两个问题,使用枚举除了线程安全和防止反射强行调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,《EffectiveJava》Item3中推荐尽可能地使用枚举来实现单例。
但是在Android中却不推荐这种用法,在Android官网Manage Your App's Memory中有这样一段话:
Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.
意思就是枚举类这种写法虽然简单方便,但是内存占用上是静态变量的两倍以上,所以尽可能的避免这种写法。
不过网上有的建议是如果程序不是大量采用枚举,那么这种性能的体现是很小的,基本不会受到影响,不用特别在意。如果程序出现了性能问题,理论上这个地方就是一个性能优化点。
10.单例模式的扩展
(1)定义
上文的几种实现方式里,一个类都只产生一个对象。万一有天产品提的需求中,需要一个类只产能产生两三个对象呢?该怎么实现?
这种需要产生固定数量对象的模式就叫做多例模式,实际上就是单例模式的自然推广,作为对象的创建模式,多例模式有以下的特点:
多例类可有多个实例;
多例类必须自己创建,管理自己的实例,并向外界提供自己的实例。
(2)应用实例
喜欢打麻将的同学(捂脸)都知道,每一桌麻将牌局都需要两个骰子,因此骰子就应该是多例类,这里就以这个场景为例来说明多例模式的应用。
实现代码为:
public class Die { private static Die die1 = new Die(); private static Die die2 = new Die(); private Die() { } public static Die getInstance(int whichOne) { if(whichOne == 1) { return die1; } else { return die2; } } public synchronized int dice() { Random rand = new Random(System.currentTimeMillis()); int value = rand.nextInt(6); value += 1; return value; } } |
在多例类Die中,使用了饿汉式方式创建了两个Die的实例,根据静态工厂方法的参数,工厂方法返还两个实例中的一个,Die对象调用die()方法代表掷骰子,这个方法会返还一个1--6之间的随机数,相当于骰子的点数。
(3)实践原则
一个多例类可以使用静态变量存储所有的实例,特别是实例数目不多的时候,可以使用一个个的静态变量存储一个个的实例。当数目较多的时候,就需要使用Map等集合存储这些实例。
使用这种模式可以让我们在设计时决定在内存中有多少个实例,方便系统进行扩展,修正单例可能存在的性能问题,提高系统的相应速度。例如读取文件,我们可以在系统启动时完成初始化工作,在内存中启动固定数量的reader实例,然后在需要读取文件时就可以快速响应。
11.小结
最后总结一下,不管哪种方案,时刻牢记单例模式的三大要点:
· 线程安全
· 延迟加载
· 序列化与反序列化安全
本文详细的分析了懒汉式,饿汉式,双重检查锁定,静态内部类,枚举五种方式的具体实现原理和优缺点,并简要介绍了单例模式的扩展--多例模式。希望大家看完之后能对单例模式有进一步的了解,并在日常工作中结合具体需求选择适合的单例模式实现。