目录
上述代码 GitHub 地址:https://github.com/baicun/designPatterns
单例模式:
保证在一个JVM中,该对象只有一个实例存在,并提供一个全局的访问点。
单例模式特点:
1. 一些类创建比较频繁,对于一些大型复杂的对象,这是一笔很大的系统开销。
2. 省去了 new 操作符,降低了系统内存的使用频率,减轻GC压力。
3. 便于管理,一个系统有且只有一个实例。
单例模式应用:
在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。也就是说凡是涉及资源的使用就一定会出现多个使用一个的问题,这样容易导致冲突,结果不一致的问题,所以单例的应用是普遍的有效的。
单例实例:
类图:
先看一个最简单的单例模式:
懒汉模式
public class Singleton {
// 私有的静态实例
private static Singleton instance = null; //懒汉模式
// 私有构造方法
private Singleton(){
}
// 静态工厂创建
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
简单的单例模式完成,在多线程环境下会出现一个bug,例如同时俩个线程进来,第一个线程判断 instance == null,则去创建对象,此时第二个线程进来,在第一个线程未创建对象前,第二个线程同样又去创建新对象,问题出现了...。粗暴的改进一下,在创建静态工厂的时候增加关键字 synchronized(同步的) 。如下:
// 静态工厂创建
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
这样做一点问题也没有,但是作为一名优秀的程序员就要做到精益求精,不要留下烂代码...不然以后阅读你代码的人一定会骂娘
首先分析一下为什么这么写不好,synchronized 加在整个方法上边,也就是说我们的项目只要实例化这个类,调用 getInstance() 就会锁住这个对象,多线程并发的时候每一个线程都要先锁住这个对象,然后判断(instance == null)是否初始化,毋庸置疑这样写不够健壮,非常影响性能。
不在方法上直接加关键字 synchronized ,那继续细化,在单例没有初始化为 null 的时候锁定这个对象,代码如下:
// 静态工厂创建
public static Singleton getInstance() {
if (instance == null) {
synchronized (instance) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
这样写就解决了上面并发加锁的问题,我刚开始学习的时候,以为这就完美了。
后来我才知道 JVM 并不能保证线程执行的顺序,也就是说如果有俩个线程同时调用我们的这个实例类,线程 A 锁定实例,并且instance = new Singleton(); 这时线程 B 进来发现 instance 不是 null ,直接返回对象,这里可能就出现了问题,为什么?分析一下【instance = new Singleton()】JVM 初始化的顺序:类的静态成员 --> 类的实例成员 --> 类的构造方法。也就是说在执行构造方法前,线程 B 进来了,并且直接返回使用,这时其实 JVM 还没有线程 A 初始化完成。
对于上述线程顺序无法保证,这里可以使用如下模式:
双重检测加锁模式:
private volatile static Singleton instance = null;//volatile 原子性,有序性,可见性
// 私有构造方法
private Singleton(){
}
// 静态工厂创建
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
并发情景:
1.线程 A 调用 getInstance(),并且进入锁定块,开始初始化,
2.线程 B 进来,开始 instance == null,进入锁定块,此时已经被线程 A 锁定,
3.线程 A 初始化完成,退出解除锁,
4.线程 B 进入锁定,此时已经存在实例对象,退出解除锁,
5.线程 A 获得实例对象,线程 B 获得线程A 实例的对象。
对于上述初始化的问题呢就有了饿汉模式,内部静态模式,枚举模式出现,如下图:
饿汉模式:
// 私有的静态实例
private static Singleton instance = new Singleton(); //饿汉模式
// 私有构造方法
private Singleton() {
}
// 静态工厂创建
public static Singleton getInstance() {
return instance;
}
内部静态模式:
// 私有的静态实例
private static Singleton instance = null;
// 内部静态代码块
static {
instance = new Singleton();
}
// 私有构造方法
private Singleton() {
}
// 静态工厂创建
public static Singleton getInstance() {
return instance;
}
枚举模式:
public enum EnumSingleton {
INSTANCE;
public void read(){
System.out.println("read");
}
}
综上所述,不同应用场景使用不同的模式,我个人推荐内部静态模式,双重检测加锁模式和枚举模式。
那个单例模式最安全?
在《effective java》一书中,作者提到单例模式是保证线程安全和单一实例的最佳实践。有俩种代码攻击类型,反射攻击和反序列化攻击,以下分别来看一下。
1. 反射攻击:
//************************【反射攻击单例模式比较 -start】***************************************************************
// 测试反射机制下的“双重检测加锁模式”
/*public static void main(String[] args) throws Exception {
Singleton sg1=Singleton.getInstance();
Singleton sg2=Singleton.getInstance();
System.out.println("正常情况下,获得的两个实例是否相同:"+(sg1==sg2));
// 获取所有构造器
Constructor<Singleton> constructor=Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton s2=constructor.newInstance();
System.out.println(sg1+"\n"+sg2+"\n"+s2);
System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(sg1==s2));
//输出结果
//正常情况下,实例化两个实例是否相同:true
//通过反射攻击单例模式情况下,实例化两个实例是否相同:false
}*/
// 测试反射机制下的“枚举单例模式”
/*public static void main(String[] args) throws Exception{
EnumSingleton singleton1=EnumSingleton.INSTANCE;
EnumSingleton singleton2=EnumSingleton.INSTANCE;
System.out.println("正常情况下,实例化两个实例是否相同:"+(singleton1==singleton2));
Constructor<EnumSingleton> constructor= null;
constructor = EnumSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
EnumSingleton singleton3= null;
singleton3 = constructor.newInstance();
System.out.println(singleton1+"\n"+singleton2+"\n"+singleton3);
System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(singleton1==singleton3));
// 输出结果
//正常情况下,实例化两个实例是否相同:true
//Exception in thread "main" java.lang.NoSuchMethodException: com.example.singletonPattern.EnumSingleton.<init>()
//会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。所以枚举是不怕发射攻击的。
}*/
//经过比较,枚举单例模式不会受到反射攻击
//************************【反射攻击单例模式比较 -end】***************************************************************
2. 反序列化攻击
//************************【反序列化攻击单例模式比较 -start】***************************************************************
// 测试反序列化机制下的“双重检测加锁模式”
/*public static void main(String[] args) throws Exception {
Singleton instance=Singleton.getInstance();
byte[] serialize = SerializationUtils.serialize(instance);
Singleton newInstance = (Singleton) SerializationUtils.deserialize(serialize);
System.out.println("反序列化情况下,两个实例是否相同:"+(instance==newInstance));
//输出结果
//反序列化情况下,两个实例是否相同:false
}*/
// 测试反射机制下的“枚举单例模式”
public static void main(String[] args){
EnumSingleton enumSingleton = EnumSingleton.INSTANCE;
byte[] serialize = SerializationUtils.serialize(enumSingleton);
EnumSingleton newInstance = (EnumSingleton) SerializationUtils.deserialize(serialize);
System.out.println("反序列化情况下,两个实例是否相同:"+(enumSingleton==newInstance));
//输出结果
// 反序列化情况下,两个实例是否相同:true
}
//经过比较,枚举单例模式不会受到反序列化攻击
//************************【反序列化攻击单例模式比较 -end】***************************************************************
通过上面方法测试,枚举模式在安全方面的确无懈可击,实现也非常简单,但不同的场景、项目应用,选择适合的就好