目录
双重检查锁定——DCL(Double Check Lock)
什么是单例
单例模式(Singleton Pattern)被认为是最简单、最易理解的设计模式。但事实上,要用好、用对它并不是一件简单的事。
那么什么是单例模式呢?它用于产生一个类的实例,并确保系统中该类有且只有这一个实例。
可能你会觉得没怎么接触过单例模式,其实我们日常使用的Windows系统就有很多地方应用单例模式:
- 任务管理器就是很典型的单例模式,你无法打开两个任务管理器窗口。
- 回收站也是单例应用,在系统运行过程中,回收站一直维护这仅有的一个实例。
- 时间计数器,系统需要保证运行过程中时间的唯一性。
单例的利与弊
在Java语言中,单例能为我们能带来两大好处:
- 对于频繁使用的对象,可以省略创建对象所花费的时间,尤其是那些重量级对象
- 由于new操作的次数减少,对系统内存的使用频率也会降低,这将减轻GC(垃圾回收)的压力
单例的弊端:
- 由于单例模式没有抽象层,使得单例类的扩展有很大的困难
- 单例类的职责过重,违背了“单一职责原则”
- 滥用单例将带来一些负面问题,比如实例化的对象长时间不使用,会被系统认为是垃圾而回收,导致对象状态的丢失
单例模式实现
单例模式有3个特点:
- 单例类只有一个实例对象
- 该实例对象只能由单例类自行创建
- 单例类需要对外提供一个访问该实例对象的全局访问点
用代码描述即:
- 私有化该类的构造方法
- 在本类中创建一个本类对象
- 定义一个公有方法,将在本类中创建的对象返回
单例模式通常有两种实现方式:懒汉式单例和饿汉式单例。
懒汉式单例
懒汉式单例的特点在‘懒’,什么是懒?懒就是能拖就拖,可以待会儿做的事情绝不在现在做。‘懒’这个字也道出了懒汉式单例的特点:只有在第一次被用到的时候才创建实例。因此懒汉式单例是延迟加载的。
public class LazySingleton {
// 类加载时不创建实例
private static LazySingleton instance;
// 私有构造方法
private LazySingleton() {}
// 第一次调用此方法时,创建实例对象
public static LazySingleton getInstance() {
if (null == instance) {
instance = new LazySingleton();
}
return instance;
}
}
LazySingleton在单线程下可以保证只存在一个实例对象,但是在多线程中却无法保证实例对象的唯一性。
public class TestLazySingletonByThread extends Thread {
@Override
public void run() {
System.out.println("Thread: " + Thread.currentThread().getName()
+ ", " + LazySingleton.getInstance().hashCode());
}
public static void main(String[] args) {
for(int i = 0; i < 10; i++) {
new TestLazySingletonByThread().start();
}
}
}
打印结果如下(每次的打印结果会略有差异):
Thread: Thread-1, 1259307936
Thread: Thread-2, 693535270
Thread: Thread-8, 1259307936
Thread: Thread-5, 1259307936
Thread: Thread-6, 1259307936
Thread: Thread-4, 1259307936
Thread: Thread-3, 424549596
Thread: Thread-0, 1259307936
Thread: Thread-7, 1259307936
Thread: Thread-9, 1259307936
我们没有重写LazySingleton的hascode()方法,因此如果是同一个LazySingleton实例对象调用hascode()方法,返回的值肯定是一样的。然而打印结果中共出现3种不同的code值,这说明在10个线程中出现了3个LazySingleton实例。
要解决这个问题很简单,我们只需要将getInstance()修改为同步方法即可:
public static synchronized LazySingleton getInstance()
但是这样又会带来性能问题——如果同时存在多个线程获取LazySingleton实例,那么在其中一个线程获取实例的时间内,其余线程都会进入等待状态。
因此我们不能同步整个getInstance()方法,只同步关键代码块。
public class LazySingletonV2 {
private static LazySingletonV2 instance;
private LazySingletonV2() {}
public static LazySingletonV2 getInstance() {
if (null == instance) {
// 不同步方法,只同步关键代码块
synchronized (LazySingletonV2.class) {
instance = new LazySingletonV2();
}
}
return instance;
}
}
在多线程中测试LazySingletonV2能否保证在程序运行时实例对象的唯一性。
public class TestLazySingletonV2ByThread extends Thread {
@Override
public void run() {
System.out.println("Thread: " + Thread.currentThread().getName()
+ ", " + LazySingletonV2.getInstance().hashCode());
}
public static void main(String[] args) {
for(int i = 0; i < 10; i++) {
new TestLazySingletonV2ByThread().start();
}
}
}
打印结果如下(每次的打印结果会略有差异):
Thread: Thread-0, 702608182
Thread: Thread-9, 41609535
Thread: Thread-5, 41609535
Thread: Thread-6, 41609535
Thread: Thread-2, 1259307936
Thread: Thread-8, 956620190
Thread: Thread-7, 41609535
Thread: Thread-1, 1259307936
Thread: Thread-4, 1259307936
Thread: Thread-3, 1259307936
通过打印结果,我们可以发现在多线程中仍然创建了多个LazySingletonV2实例对象。之所以会出现这种情况,是因为如果多个线程同时到达同步代码块的位置,那么在进入同步代码块之前的判断instance是否为null就失去了作用。
两个线程调用如下:
Time | Thread A | Thread B |
---|---|---|
T1 | 检查到instance 为null | 检查到instance为null |
T2 | 获取锁 | 等待 |
T3 | 为instance分配实例对象(第一个实例) | 等待 |
T4 | 释放锁 | 获取锁 |
T5 | 为instance分配实例对象(第二个实例) | |
T6 | 释放锁 |
双重检查锁定——DCL(Double Check Lock)
针对上面的情况,我们需要在同步代码块中再一次对instance判null,也就是所谓的“双重检查锁定”。
public class LazySingletonV3 {
private static LazySingletonV3 instance;
private LazySingletonV3() {}
public static LazySingletonV3 getInstance() {
if (null == instance) {
synchronized (LazySingletonV3.class) {
// 再一次对instance判null
if (null == instance) {
instance = new LazySingletonV3();
}
}
}
return instance;
}
}
使用双重检查锁定基本上就可以解决懒汉式单例在多线程中问题。为什么要说“基本上”呢?上述写法看似解决了问题,实则有个很大的隐患——实例化对象的那行代码(instance = new LazySingletonV3()),实际上可以分解成以下三个步骤:
- 为实例对象分配内存空间
- 初始化对象
- 将引用指向刚分配的内存空间
但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,顺序就变成:
- 为实例对象分配内存空间
- 将引用指向刚分配的内存空间
- 初始化对象
考虑重排序后,两个线程发生了以下调用:
Time | Thread A | Thread B |
---|---|---|
T1 | 检查到instance为null | |
T2 | 获取锁 | |
T3 | 再次检查到instance为null | |
T4 | 为实例对象分配内存空间 | |
T5 | 将instance指向内存空间 | |
T6 | 检查到instance不为null | |
T7 | 返回并访问instance(此时实例对象还未完成初始化) | |
T8 | 初始化实例对象 | |
T9 | 释放锁 |
为了解决上述问题,需要用关键字volatile修饰instance。使用volatile修饰后,重排序被禁止,所有对instance的写操作都将发生在读操作之前。至此双重检查锁定就可以正常运行了。
饿汉式单例
饿汉式单例也很容易理解,饥饿的人肯定急着要吃饭,这种单例模式的特点体现在‘急’上,它会在类初始化(调用类中静态方法)时就完成对象的实例化。由于JVM会保证类的初始化只会进行一次,因此饿汉式单例模式避免了多线程的同步问题。
public class HungrySingleton {
private static final HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {}
// 调用该方法会初始化HungrySingleton类
public static HungrySingleton getInstance() {
return instance;
}
}
注:用final关键字修饰instance并不是必须的,只是因为单例类的实例对象只需要创建一次,所以用final修饰更为严谨。
我在网上查阅很多资料,其中很多都认为饿汉式单例在类加载时创建实例对象,会占用内存资源,但事实并不是这样。
类加载分为三个阶段:加载、连接、初始化,在初始化阶段才会为静态变量赋初始值。类的加载由JVM的具体实现决定,但是类的初始化只有出现下面7种情况之一才会发生:
- 创建类的实例
- 访问某个类、接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射
- 初始化一个类的子类
- JVM启动时被标记为启动类的类(包含main()方法)
- JDK1.7开始提供动态语言支持(很少使用)
很明显,想要初始化HungrySingleton类,正常情况下只有通过调用该类中的getInstance()方法才可以。因此如果不调用单例类对外暴露的静态方法,就不会创建HungrySingleton实例对象,也就不存在占用内存资源一说。
换而言之,饿汉式和懒汉式一样,都是延迟加载的。
懒汉式与饿汉式的不同
在饿汉式单例中,我提到饿汉式和懒汉式都是延迟加载的,而且饿汉式还不用担心多线程的同步问题,那么懒汉式还有什么存在的必要呢?
作为一个类应该要纯粹,然而现实往往不是这样。假如懒汉式单例类LazySingleton和饿汉式单例类HungrySingleton中除了获取单例对象的静态方法getInstance(),还存在下面的静态方法:
public static void sayHello() {
System.out.println("hello");
}
分别通过两个类调用该方法,那么LazySingleton类并不会创建实例对象,而HungrySingleton类会创建实例对象。
所以在这种情况下,懒汉式依然可以保持延迟加载,等到调用getInstance()方法时才会创建实例对象;而饿汉式则会立刻创建实例对象,出现浪费内存资源的情况。
其他单例模式实现
除了上述两种主要的单例实现方式,还有其他几种单例的实现方式。
静态内部类单例
静态内部类单例可以看做是懒汉式单例的一种变式。这种单例模式一方面具有懒汉式单例延迟加载的特性,另一方面又避免了懒汉式单例在多线程中的同步问题。可谓“取其精华,去其糟粕”,该实现是比较推荐的一种做法。
静态内部类单例解决多线程同步问题的方式和饿汉式单例一样,利用JVM只会初始化类一次,使用静态内部类的静态成员变量记录单例类的实例对象。
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() {}
public static StaticInnerClassSingleton getInstance() {
return InstanceHolder.instance;
}
private static class InstanceHolder {
private static StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
}
}
枚举单例
枚举类型是有“实例控制”的类,确保不会同时存在两个实例,也就是说在任何情况下都是单例。枚举单例的优点就是简单,但是大部分应用开发很少用枚举,可读性并不是很好。
public enum EnumSingleton {
INSTANCE;
public void doSomething() {
// TODO
}
}
登记式单例
登记式单例的作用是将多种单例类统一管理,使用时根据相应的key获取对应的单例对象。这种实现方式的好处是在对用户隐藏具体实现、降低代码耦合度的同时,降低了用户的使用成本。简易版代码实现如下:
public class SingletonManger {
private static Map<String, Object> objMap = new HashMap<>();
private SingletonManger() {}
public static void registerService(String key, Object instance) {
if (objMap.get(key) != null) {
objMap.put(key, instance);
}
}
public static Object getService(String key) {
return objMap.get(key);
}
}
破坏单例的方式及解决方法
诚然我们的本意是希望通过单例模式保证系统中只存在一个实例对象,但是世界上没就没有什么完美的东西,单例模式也并不完美,下面几种方式就可以破坏单例。(注:以下几种破坏单例的方式对枚举单例无效。)
反序列化
我们知道,序列化可以将一个实例对象写到磁盘,实现数据的持久化,也能实现实例对象数据的远程传输。
那么如果单例类实现Serializable接口,即使私有化它的构造方法,在反序列化单例对象时依然会通过特殊的途径再创建一个新的实例对象,相当于调用了单例类的构造方法获得一个新实例。见下面一个例子:
public class SerSingleton implements Serializable {
private static final long serialVersionUID = 417892990361624082L;
private static SerSingleton instance = new SerSingleton();
private SerSingleton() {}
public static SerSingleton getInstance() {
return instance;
}
public static void main(String[] args) {
// 通过SerSingleton提供的getInstance()方法获取实例
SerSingleton singleton = SerSingleton.getInstance();
SerSingleton singleton2 = SerSingleton.getInstance();
System.out.println("singleton == singleton2: " + (singleton == singleton2));
try {
// 序列化SerSingleton实例
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("serfile/SerSingleton.ser"));
oos.writeObject(singleton);
oos.close();
// 反序列化
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("serfile/SerSingleton.ser"));
SerSingleton deserializeSingleton = (SerSingleton) ois.readObject();
ois.close();
// 反序列化结果与实例对比
System.out.println("singleton == deserializeSingleton: "
+ (singleton == deserializeSingleton));
} catch (Exception e) {
e.printStackTrace();
}
}
}
打印结果如下:
singleton == singleton2: true
singleton == deserializeSingleton: false
通过打印结果我们发现,反序列化可以破坏单例模式。所以为了避免单例对象在反序列化时重新生成对象,需要在实现Serializable接口的同时实现readResolve()方法(定义于ObjectStreamClass类中),以保证反序列化的时候获得原来的对象。
readResolve()是反序列化操作提供的一个钩子函数,它在从流中读取对象的readObject() 方法执行之后被调用,可以让开发人员控制对象的反序列化。我们只需要在readResolve()方法中用instance替换掉从流中读取后新创建的实例对象,就可以避免使用序列化对单例模式的破坏。
在SerSingleton类中加入readResolve()方法:
private Object readResolve() {
return instance;
}
再次运行程序,打印结果如下:
singleton == singleton2: true
singleton == deserializeSingleton: true
反射
除了反序列化,反射也可以破坏单例。反射可以通过setAccessible(true)使权限检查失效,从而调用单例类的私有构造方法达到创建对象的目的。
public class ReflectSingleton {
private static ReflectSingleton instance = new ReflectSingleton();
private ReflectSingleton() {}
public static ReflectSingleton getInstance() {
return instance;
}
public static void main(String[] args) {
// 通过ReflectSingleton提供的getInstance()方法获取实例
ReflectSingleton singleton = ReflectSingleton.getInstance();
ReflectSingleton singleton2 = ReflectSingleton.getInstance();
System.out.println("singleton == singleton2: " + (singleton == singleton2));
try {
Constructor<ReflectSingleton> constructor =
ReflectSingleton.class.getDeclaredConstructor();
// 通过反射使权限检查失效
constructor.setAccessible(true);
ReflectSingleton reflectSingleton = constructor.newInstance();
// 反射生成实例与单例实例对比
System.out.println("singleton == reflectSingleton: "
+ (singleton == reflectSingleton));
} catch (Exception e) {
e.printStackTrace();
}
}
}
打印结果如下:
singleton == singleton2: true
singleton == reflectSingleton: false
为了防止这种情况的发生,我们需要改进单例类的构造方法。在单例类的构造方法中对instance进行判null处理,一旦有第二次创建单例对象的行为发生,就抛出异常(注:在构造方法中只能抛出非受检查异常,即RuntimeException及其子类)。
对ReflectSingleton的构造方法进行改造:
private ReflectSingleton() {
if (null != instance) {
throw new RuntimeException("cannot create instance more");
}
}
再次运行程序,打印结果如下:
singleton == singleton2: true
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:423)
at org.hu.singleton.ReflectSingleton.main(ReflectSingleton.java:29)
Caused by: java.lang.RuntimeException: cannot create instance more
at org.hu.singleton.ReflectSingleton.<init>(ReflectSingleton.java:11)
... 5 more
克隆
clone()方法定义于Object类中,我们知道Java中每个类都是Object的子类,所以单例类也继承了clone()方法。clone()方法并不是通过调用构造方法创建对象,而是直接拷贝内存区域。因此若单例类实现了Cloneable接口,尽管其构造方法是私有的,仍然可以通过clone()方法创建一个新的单例实例,从而让单例模式失效。
public class CloneSingleton implements Cloneable {
private static CloneSingleton instance = new CloneSingleton();
private CloneSingleton() {}
public static CloneSingleton getInstance() {
return instance;
}
public static void main(String[] args) {
// 通过CloneSingleton提供的getInstance()方法获取实例
CloneSingleton singleton = CloneSingleton.getInstance();
CloneSingleton singleton2 = CloneSingleton.getInstance();
System.out.println("singleton == singleton2: " + (singleton == singleton2));
try {
// 通过clone()创建CloneSingleton实例
CloneSingleton cloneSingleton = (CloneSingleton) singleton.clone();
// 反序列化结果与实例对比
System.out.println("singleton == cloneSingleton: "
+ (singleton == cloneSingleton));
} catch (Exception e) {
e.printStackTrace();
}
}
}
打印结果如下:
singleton == singleton2: true
singleton == cloneSingleton: false
针对这种情况,我们只需要重写clone()方法即可。在CloneSingleton类中添加下面的代码:
@Override
protected Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
可能你会觉得奇怪——为什么要大费周章的复写clone()方法,不让单例类实现Cloneable接口不就可以了吗?确实作为一个单例类不应该实现Cloneable接口,但是虽然很少见,有时单例类会存在父类。如果单例类的父类实现了clone()方法,那么我们就需要在单例类中复写clone()方法避免对单例模式的破坏。
参考: