单例模式,多是惟一一个咱们谈到时,每一个工程师都会二眼放光,口若悬河的模式,除了它最简单直接外,还由于咱们“自觉得”对它了如指掌,这篇文章带你们作个总结,死磕单例模式的方方面面。java
大概有如下二种场景须要单例安全
实现一个单例,咱们要考虑如下几点。bash
总结一句话,如何线程安全的建立惟一实例对象。 先看一下Java中如何具体实现单例。app
public class UserManager {
private static UserManager instance = new UserManager();
private UserManager() { }
public static UserManager getInstance() {
return instance;
}
}
复制代码
首先经过私有化构造器,禁止了外部new的可能性,而后instance是static修饰的,因此在类被首次加载后,调用init 的时候,instance会被初始化,JVM保证类加载过程的线程安全,因此instance也是线程安全的。 由于在类加载初始化的时候,单例就被建立出来了,因此相对于按需延时加载,这种写法若是有大量单例须要建立,在系统刚启动时内存压力比较大。同时上面的写法也没有可以禁止序列化和反射对单例的破坏(关于这个咱们放到最后来解决)。ide
private static volatile UserManager instance;
private UserManager() {}
public static UserManager getInstance() {
if (instance == null) {
synchronized (UserManager.class) {
if (instance == null) {
instance = new UserManager();
}
}
}
return instance;
}
复制代码
这也是很经典的单例实现,经过二次判空检查,并且只有在第一次初始化时getInstance会加锁,后面的获取都不会加锁,时间和空间效率都很高。 这里要注意的一点是instance必定要加volatile修饰符。关于这一点,不少同窗可能会理解的不够全面,下面我来详细分析一下。 首先由于在建立UserManager的时候,咱们是有加锁的,并且锁的对象是UserManager这个Class对象。好比线程A得到了锁,开始new UserManager(), 而且赋值给了instance,这时候线程B开始调用getInstance()来获取单例对象,因为锁拥有可见性,因此线程A的赋值happen-before线程B的获取,表面上看一切很完美,可是在jdk1.5以前,volatile语意尚未被增强,不能禁止指令重排序。ui
instance = new UserManager();
复制代码
这条语句,其实能够被看作三条伪代码。spa
private UserManager() {}
private static UserManager getInstance() {
return SingltonHolder.sInstance;
}
private static class SingltonHolder {
private static UserManager sInstance = new UserManager();
}
复制代码
静态内部类的方式实现的单例一样是线程安全的,由JDK来保证。同时也具备延时加载的特性。这种写法对比Double-Check更简洁,推荐使用。线程
Effective Java中推荐使用枚举的方式来实现单例,咱们来看一下code
public enum UserManager {
INSTANCE;
}
复制代码
很简洁,但咱们知道,枚举是Java提供的语法糖,咱们解语法糖看下它的具体代码对象
public final class com.dig.deep.design.singlton.UserManager extends java.lang.Enum<com.dig.deep.design.singlton.UserManager> {
public static final com.dig.deep.design.singlton.UserManager INSTANCE;
private static final com.dig.deep.design.singlton.UserManager[] $VALUES;
public static com.dig.deep.design.singlton.UserManager[] values();
public static com.dig.deep.design.singlton.UserManager valueOf(java.lang.String);
private com.dig.deep.design.singlton.UserManager();
static {};
}
复制代码
能够看到解语法糖后的UserManager,构造器也是私有的,有个一个static final 的INSTANCE类常量,能够大胆猜想,JVM在加载枚举类时,会给全部的枚举项赋值,同时会保证过程的线程安全。
咱们上面有提到过,一个完整的单例须要作到防止
try {
UserManager userManager = UserManager.instance;
FileOutputStream fileOutputStream = new FileOutputStream("user");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(userManager);
FileInputStream fileInputStream = new FileInputStream("user");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
UserManager newUserManager = (UserManager) objectInputStream.readObject();
System.out.println("is equal: " + (userManager == newUserManager));
} catch (IOException e) {
e.printStackTrace();
}
复制代码
输出是false,通过序列化和反序列化后,生成了二个单例对象,显然破坏了单例的语意,解决这个问题,咱们能够给UserManager增长一个readResolve方法, 并在其中返回单例对象。
private Object readResolve() {
return instance;
}
复制代码
Class clazz = UserManager.class;
Constructor[] constructors = clazz.getDeclaredConstructors();
try {
constructors[0].setAccessible(true);
UserManager newUserManager = (UserManager) constructors[0].newInstance();
} catch (Exception e) {
e.printStackTrace();
}
复制代码
若是开发者真的使用反射来做恶,谁能拦得住呢?虽然反射最终调用的仍是咱们的私有构造器,在构造器里面咱们能够加一些判断逻辑,可是仍是不能涵盖全部的状况,由于毕竟咱们的单例实现多种多样,有延时加载的,有非延时加载的。 可是经过Enum方式实现的单例是不可以被反射的,若是尝试反射Enum的构造器,会抛出一个异常,因此Enum方式实现的单例对反射安全。
尽可能不要给单例实现cloneable接口,若是非要实现,也在重写的clone方法里,返回此单例对象。
@Override
protected Object clone() throws CloneNotSupportedException {
return getInstance();
}
复制代码
单例模式比较简单,同时咱们平常工做也用的很频繁,工程师有必要对它有个全面了解,在选择实现方案时作到心中有数。