单例模式是开发过程中常用的模式之一,首先了解下单例模式的四大原则:
- 构造方法私有;
- 以静态方法或枚举返回实例;
- 确保实例只有一个,尤其是多线程环境;
- 确保反射或反序列化时不会重新构建对象;
饿汉模式
饿汉模式在类被初始化时就创建对象,以空间换时间,故不存在线程安全问题。
public class SingleTon{
private static SingleTon INSTANCE = new SingleTon();
private SingleTon(){}
public static SingleTon getInstance(){ return INSTANCE; }
}
饱汉模式
饱汉是变种最多的单例模式。我们从饱汉出发,通过其变种逐渐了解实现单例模式时需要关注的问题。
基础饱汉
public class Singleton {
private static Singleton singleton = null;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
饱汉模式的核心就是懒加载。好处是更启动速度快、节省资源,一直到实例被第一次访问,才需要初始化单例;缺点是线程不安全,if语句存在竞态条件。
DCL(Double Check Lock)
上面说到基础饱汉模式的缺点是线程不安全,在多线程环境下无法保证实例唯一,因此只需要在关键语句上加锁即可解决问题:
public class Singleton {
private static Singleton singleton = null;
private Singleton() {}
public synchronized static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
第一种解决方式简单粗暴,直接在获取实例的方法上加锁,保证了多线程情况下实例唯一,但是synchronized操作是很耗时的,导致的结果就是并发性能极差,因此可对其进行修改:
public class Singleton {
private static Singleton singleton = null;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized(Singleton.class){
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
这就是双重检查锁模式(DCL,Double Check Lock),在这种模式下,基本达到了理想的效果(懒加载+线程安全);
事实上,DCL模式仍存在一些问题,第11行中singleton = new Singleton()并不是一个原子操作,它在jvm中分为3步执行:
- memory = allocate(); //在堆内存开辟内存空间;
- initInstance(memory); // 在堆内存中实例化SingleTon里面的各个参数;
- instance = memory; //把对象指向堆内存空间。
由于jvm指令重排的机制,第3步操作是可能被优化到第2步操作之前的,此时若有新的线程进入该方法,经if判断后(因第3步操作使该对象指向了一块内存空间,所以if判断结果为false)会直接返回singleton,即引用instance指向内存memory时,该内存还未被初始化;这就是著名的DCL失效问题,解决方法也很简单,使用volatile关键字修饰即可;
Holder模式
我们既希望利用饿汉模式中静态变量的方便和线程安全;又希望通过懒加载规避资源浪费。Holder模式满足了这两点要求:核心仍然是静态变量,足够方便和线程安全;通过静态的Holder类持有真正实例,间接实现了懒加载。
public class Singleton {
private static class SingletonHolder {
private static final Singleton singleton = new Singleton();
private SingletonHolder() {
}
}
private Singleton() {}
public static Singleton getInstance() {
return SingletonHolder.singleton;
}
}
静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。即当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
虚拟机会保证一个类的在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的方法,其他线程都需要阻塞等待,直到活动线程执行初始化方法完毕。
枚举模式
public enum Singleton {
SINGLETON;
}
枚举在java中与普通类一样,都能拥有字段与方法,而且枚举实例创建是线程安全的,在任何情况下,它都是一个单例;但是缺点是可读性极差。 通过反编译我们可以看到枚举类的本质:
本质上和饿汉模式相同,区别仅在于公有的静态成员变量。
反射与反序列化攻击
以上介绍的几种模式,除了枚举模式外,其他模式均无法防止反射攻击和反序列化攻击;
反射演示:
public static void main(String[] args) throws Exception{
SingleTon singleTon = SingleTon.getInstance();
Constructor<SingleTon> constructor = SingleTon.class.getDeclaredConstructor();
constructor.setAccessible(true);
SingleTon singleTon1 = constructor.newInstance();
System.out.println(singleTon1 == singleTon);
}
解决办法:反射是通过构造方法构建实例的,那么只需要在构造方法中加入判空即可:
反序列化演示(需单例类实现Serializable接口):
public static void main(String[] args) throws Exception{
SingleTon singleTon = SingleTon.getInstance
FileOutputStream out = new FileOutputStream("SingleTon.txt");
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(singleTon);
oos.close();
out.close
FileInputStream in = new FileInputStream("SingleTon.txt");
ObjectInputStream obj = new ObjectInputStream(in);
SingleTon singleTon1 = (SingleTon) obj.readObject();
in.close();
obj.close();
System.out.println(singleTon == singleTon1);
}
解决办法:定义readResolve()方法,反序列化时,如果定义了readResolve()则直接返回此方法指定的对象。而不需要单独再创建新对象。
|