前言
单例模式的线程安全性主要考虑多线程同步问题以及反射不安全的问题。
线程安全的三种单例模式
1. 枚举
枚举类是天生线程安全的,可以避免多线程同步问题。
普通类的反序列化时通过反射找到类的默认构造函数来进行对象初始化。而枚举类不是通过反射来进行反序列化的,因此可以防止由于反射破坏单例模式的问题。
public enum Singleton{
INSTANCE;
}
2. 双重校验锁
public class Singleton {
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) { // 确保instance没有被创建
synchronized (Singleton.class) { // 确保不会出现多个线程同时创建instance
if (instance == null) { // 确保在获取锁的过程中没有instance创建
instance = new Singleton();
}
}
}
return instance;
}
}
为什么instance用static修饰?
- static类型的instance存在静态存储区,每次调用时,都指向的同一个对象。
- 静态方法
getInstance()
只能访问静态对象。 - instance需要在调用
getInstance()
时候被初始化,只有static的成员才能在没有创建对象时进行初始化。且类的静态成员在类第一次被使用时初始化后就不会再被初始化,保证了单例。
为什么instance用volitale修饰?
为了防止instance new的过程中指令重排
具体而言,new Singleton()
这个指令分了三步进行:
- 为
instance
分配内存空间 - 初始化
instance
- 将
instance
指向分配的内存地址
如果是1.3.2.的话,当线程执行了1.3.后其他线程发现instance!=null
,误认为执行完毕,会导致其他线程获取到一个未初始化的对象。
3. 静态内部类
public class Singleton {
private Singleton() {}
public static Singleton getInstance() {
return SingletonInner.instance;
}
// 静态内部类
private static class SingletonInner{
private final static Singleton instance = new Singleton();
}
}
为什么静态内部类实现的单例模式是线程安全的?
静态内部类实现的单例模式是通过jvm来保证线程安全的。
在进行类初始化时,jvm内部会保证一个类的clinit方法在多线程环境中的线程安全性。也就是说,如果有多个线程同时对同一个类进行初始化,jvm会通过synchronized
关键字来确保只能有一个线程进行初始化操作,并且会保证在多线程环境下对该类的初始化操作只能有一次。【jvm可以保证一个类初始化操作的只有一次且线程安全】
类初始化clinit的时机主要有:
- 创建该类的实例
- 调用该类的静态方法
- 类中的某个静态字段被赋值
- 类中的某个非常量的静态字段被使用
因此,静态内部类属于上述第四种。
第一次加载Singleton类时,Singleton类并不会初始化;只有Singleton类在调用getInstance()
静态方法时,Singleton类才会进行初始化操作。在初始化时,由于getInstance()
静态方法中包含了静态内部类,所以会导致静态内部类SingletonInner
的加载,这时会把Singleton类运行时常量池中的符号引用转为直接引用。由于jvm会保证同一个类加载器即使在多线程环境中也只会初始化一次,因此可以保证静态字段instance的单例。【利用类加载的唯一性和安全性来保证单例】