我相信99%的人接触到的第一个设计模式是单例模式,在单例模式中,单例对象的类必须保证只有一个实例存在。单例模式的设计思路是私有类的构造函数,保证当前类永远持有同一个对象的引用,并通过当前类提供的静态方法发布出去。
单例模式的优点是提供了对类唯一对象的受控访问,节约系统资源,降低频繁创建销毁带来的性能开销,避免对共享资源的重复占用。
单例模式的缺点是类的职责过重,没有抽象层,扩展性不太好。
我总结了7种单例的实现方式:
一:“饿汉式”单例
public class HangerSingleton {
private static final HangerSingleton instance = new HangerSingleton();
private HangerSingleton() {}
public static HangerSingleton getInstance() {
return instance;
}
}
“饿汉式”单例优点:
使用简单
类加载机制保证单例,线程安全
不考虑线程安全问题,性能更好
“饿汉式”单例缺点:
类加载时立即对实例进行初始化,如果实例没有被使用,会造成一定的内存浪费。
二:“懒汉式”单例
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
“懒汉式”单例优点:
使用简单
延迟加载,第一次真正使用对象的时候才会加载,不会造成内存的浪费。
“懒汉式”单例缺点:
线程不安全,多线程环境下会创建多个实例
三:线程安全“懒汉式”单例
public class ThreadSafeLazySingleton {
private static ThreadSafeLazySingleton instance;
private ThreadSafeLazySingleton (){ }
public static synchronized ThreadSafeLazySingleton getInstance() {
if (instance == null){
instance = new ThreadSafeLazySingleton();
}
return instance;
}
}
线程安全“懒汉式”单例优点:
独占锁保证线程安全
延迟加载
线程安全“懒汉式”单例缺点:
synchronized在整个方法上加锁,频繁调用加锁方法,性能较差。
四:“DCL”单例
public class DCLSingleton {
private static volatile DCLSingleton instance;
private DCLSingleton() { }
public static DCLSingleton getInstance() {
if (instance == null) {
synchronized (DCLSingleton.class) {
if (instance == null) {
instance = new DCLSingleton();
}
}
}
return instance;
}
}
“双检锁”单例优点:
线程安全
延迟加载
锁粒度小于直接在方法上加锁,实例不为空不会进入同步代码块阻塞
“双检锁”单例缺点:
依赖JDK版本,JDK1.5之前volatile不支持禁止指令重排序,因为new操作并不是一个原子性操作,JVM为了性能优化会进行指令重排序,导致其他线程获取到一个没有被正确构造的对象。
五:静态内部类单例(see JDK Math.random方法)
public class InnerClassSingletion {
private InnerClassSingletion() {
}
private static final class InstanceHolder{
private static final InnerClassSingletion instance = new InnerClassSingletion();
}
public static InnerClassSingletion getInstance() {
return InstanceHolder.instance;
}
}
“静态内部类”单例优点:
JVM本身保证线程安全
延迟加载,外部类的加载不影响内部类的加载,只有真正调用getInstance方法的时候,内部类才会被加载。
六:CAS单例
public class AtomicSingleton {
private static AtomicReference<AtomicSingleton> reference = new AtomicReference<AtomicSingleton>();
private AtomicSingleton() {}
public static AtomicSingleton getInstance() {
for (;;) {
AtomicSingleton current = reference.get();
if (current != null)
return current;
if (reference.compareAndSet(null, new AtomicSingleton())) {
return reference.get();
}
}
}
}
CAS单例优点:
通过自旋CAS操作在CPU层面保证线程安全
延迟加载
CAS单例缺点:
使用复杂
七:枚举类单例
public enum EnumSingleton {
instance;
public static EnumSingleton getInstance() {
return instance;
}
}
枚举类单例优点:
使用十分简单
不可变类保证线程安全
内存占用小
不能被实例化
能够防止通过反序列化创建对象
能够防止通过反射创建对象
能够防止通过clone创建对象
枚举类单例缺点:
不可变类扩展性差
在Java中一般来说有以下四种方式创建一个对象:
通过new关键字创建
通过反射创建
通过克隆创建
通过反序列化创建
public class Singleton implements Cloneable, Serializable {
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
@Test
public void testCreateObject() throws IllegalAccessException, InstantiationException
, ClassNotFoundException, IOException, CloneNotSupportedException {
//1.通过new关键字创建
Singleton singleton1 = new Singleton();
//2.通过反射创建
Singleton singleton2 = Singleton.class.newInstance();
//3.通过重写Cloneable接口clone方法克隆创建
Singleton singleton3 = (Singleton)singleton1.clone();
//4.通过实现Serializable接口反序列化创建
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(new File(“serialize.txt”)));
objectOutputStream.writeObject(singleton1);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File(“serialize.txt”)));
Singleton singleton4 = (Singleton) objectInputStream.readObject();
}
私有类的构造函数,只能在一定程度上避免通过“new”关键字这种方式创建对象,而不能从根本上避免多实例的创建,这七种单例模式除了枚举单例以外,其他六种都可以通过反射、克隆、反序列化方式来创建多个实例,所以说传统意义上的单例模式实现其实是不完善的。
通常说枚举类是最好的单例,主要由于它不需要开发人员通过额外的机制就能保证创建出的对象一定是单例的:
枚举类的默认构造函数是私有的,不允许其他权限修饰符来修饰枚举类的构造函数,因此不能通过实例化来创建枚举类的实例。
Constructor类中newInstance方法对枚举类进行判断,阻止枚举类通过反射创建实例。
所有枚举类的公共抽象父类都是Enum类,在Enum类中重写了clone方法,并使用final定义不能被子类重写。
Enum类的序列化和反序列化方式和一般对象的序列化方式不同,对枚举类进行序列化的时候,只会序列化它的name,反序列化通过Enum.valueOf方法同样以name反序列化为对象。并且在反序列化过程中readObject,readObjectNoData和readResolve方法都会被忽略,同样任何的serialPersistentFields和serialVersionUID同样也会被忽略,serialVersionUID固定为0L。
同样重写了readObject和readObjectNoData两个方法阻止反序列化
为了防止通过反射、克隆、反序列化的方式创建新的实例对象,我们需要对单例类进行改造:
public class UpgradeSingleton implements Serializable, Cloneable{
private static final UpgradeSingleton instance = new UpgradeSingleton();
private static volatile boolean isInitialized;
//防止反射通过newInstance攻击
private UpgradeSingleton() {
if (!isInitialized) {
synchronized (UpgradeSingleton.class) {
isInitialized = !isInitialized;
}
}else {
throw new IllegalArgumentException(“Cannot reflectively create enum objects”);
}
}
public static UpgradeSingleton getInstance() {
return instance;
}
//重写clone方法,禁止clone
@Override
public Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
//单例对象有必要序列化的话,实现readResolve方法保证反序列化为原来对象
private Object readResolve() {
return instance;
}
}
其实上面代码中的isInitialized属性也是可以通过反射进行修改的,从而破坏单例模式,但是这种通过反射刻意修改控制单例属性的场景在正常的编码场景下几乎是不会出现的。
单例模式也可以通过ThreadLocal实现保证每个线程内只有一个对象,使用指定classLoader保证多classLoader下只有一个对象以及通过登记式单例使用Map容器对单例对象进行统一管理,根据使用场景不同,选取最适合的单例模式,既要考虑到未来需求的扩展,同时也不要过度设计,最合适的就是最好的设计模式。