单例模式是java中用的比较多的一种设计模式,目的就是让一个应用中对于某个类,只存在唯一的实例化对象。单例模式有很多实现方案,各有利弊,接下来将做详细分析介绍。
饿汉模式 :利用jvm类加载机制,在类加载到jvm后就完成唯一实例的初始化
/**
* 饿汉模式
*
* @author Typhoon
* @date 2018-04-14 21:27 Saturday
*/
public class SingletonPattern {
/**
* 类加载的时候创建唯一实例
*/
private static SingletonPattern instance = new SingletonPattern();
/**
* 不允许外部引用调用构造器
*/
private SingletonPattern() {}
/**
* 返回单例
*
* @return
*/
public static SingletonPattern getInstance() {
return instance;
}
}
优点:利用jvm类加载完成初始化,不存在并发问题
缺点:如果一个应用很多种这种类,有些可能很少用到,都加在到内存中造 成压力
懒汉模式 :也即是在jvm加载类的时候并不初始化唯一实例,而是在第一次被调用的时候被初始化
/**
* 懒汉模式
*
* @author Typhoon
* @date 2018-04-14 21:38 Saturday
*/
public class Singleton2 {
private static Singleton2 instance = null;
private Singleton2() {}
public static Singleton2 getInstance() {
if(null == instance) {
instance = new Singleton2();
}
return instance;
}
}
在并发的时候,上述的懒汉模式发生了线程安全问题,实例被多次初始化
因此需要加同步控制:
再次运行测试
这次看到多线程调用已经做了同步控制
优点 :用的时候才会初始化,不会过早占用内存
缺点 :对方法做了同步控制,吞吐量有瓶颈
双重校验锁 :其实这个是基于懒汉模式的改进,用同步代码块代替同步方法来提高性能和吞吐量
/**
* 双重校验锁
*
* @author Typhoon
* @date 2018-04-14 21:59 Saturday
*/
public class SingletonPattern3 {
private static SingletonPattern3 instance = null;
private SingletonPattern3() {}
public static SingletonPattern3 getInstance() {
//并发情况下如果instance没有初始化,线程都会进synchronized块
if(null == instance) {
//第一次初始化的时候并发线程在此阻塞
synchronized (SingletonPattern3.class) {
//第一次初始化时候多个线程依次进入,肯定会有一个线程先初始化成功,这个时候如果新进来的线程不加判断,将会继续初始化
if(null == instance) {
instance = new SingletonPattern3();
}
}
}
return instance;
}
}
可以看到,双重校验锁已经解决了并发场景的唯一实例初始化问题。
优势 :既实现了懒汉模式,也解决了并发初始化问题
缺点 :存在jvm指令重排问题
静态内部类 :通过静态内部类来实现单例模式
/**
* 静态内部类
*
* @author Typhoon
* @date 2018-04-14 22:14 Saturday
*/
public class SingletonPattern4 {
private static class SingletonHolder {
public static SingletonPattern4 instance = new SingletonPattern4();
}
private SingletonPattern4() {}
public SingletonPattern4 getInstance() {
return SingletonHolder.instance;
}
}
运行结果可以看到,静态内部类也可以实现单例模式。它和饿汉模式有点类似,都是通过jvm类加载机制创建实例,所以不存在并发问题;但是和饿汉模式不一样的地方是,只要应用中不适用内部类,jvm就不会去加载这个单例类,也不会创建单例对象,从而实现了懒汉模式中的延迟加载。
优点 :不存在并发问题,延迟加载
缺点 :实现麻烦,不直观
枚举 public enum SingletonPattern5 {
INSTANCE {
@Override
protected void read() {
System.out.println("read");
}
@Override
protected void write() {
System.out.println("write");
}
};
protected abstract void read();
protected abstract void write();
}
运行测试程序,看到通过枚举也实现了单例模式。使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,《Effective Java》作者推荐使用的方法。但是,在实际工作中,很少看见有人这么写。
除了枚举的实现,其他四种实现还存在其他问题:
1)每次反序列化一个序列化的对象时都会创建一个新的实例。
2)可以使用反射强行调用私有构造器
我们使用实例来证明:
饿汉模式
懒汉模式
双重校验锁
静态内部类
枚举类
根据上述运行结果可以得知除了枚举,其他方式都存在问题。对于存在的问题如何解决呢?
1)反序列化问题:在单例中定义readResolve方法,反序列化的时候readSolve方法会被调用到,用readResolve()返回的对象直接替换掉反序列化过程中创建的对象,这样就解决了单例模式的反序列化漏洞
2)反射问题:对于反射问题,我们可以在单例类中的私有构造器中判断,如果唯一实例已经被初始化过了,直接抛异常
以饿汉模式为例:
重新运行测试:
可以看到,反序列化后和序列化之前是同一个对象,反射创建单例类对象失败。这也就解决了单例模式中的反序列化和反射漏洞。
此篇暂且分析到这里,希望能帮大家对单例模式带来更深刻的理解和认识,从而更好更安全的使用单例模式!
创作不易,请多多支持!
附带公众号: