单例定义: 确保该类有且仅有一个实例,并提供一个关于它的全局访问点
直接加载: 自始至终全局唯一不可变
延迟加载(懒加载)(load on demand): 外部调用该单例之前单例为空, 第一次外部调用开始以后, 全局唯一不可变
1 直接加载
public class Singleton {
// private: 私有,保证封装性,外部只能通过get方法访问,而不能直接访问
// static: 静态,保证该成员是属于类的(而不是对象的);如果不写static修饰,表明该成员是属于对象的,不可能是单例
// final:该成员不可变
private static final Singleton INSTANCE = new Singleton();
// private构造方法, 外部不能调用它, 即不能通过调用构造方法实例化该类
private Singleton() {
}
// 公有静态get方法,供外部访问私有实例
public static Singleton getInstance() {
return INSTANCE;
}
}
2.0 延迟加载(懒加载(Load On Demand))
public class Singleton {
// 不加final, INSTANCE尚未实例化
private static Singleton INSTANCE = null;
private Singleton() {
}
// 出现外部调用 再实例化
public static Singleton getInstance() {
if (INSTANCE == null) {
// 存在问题:多线程环境下,INSTANCE容易被多次实例化
INSTANCE = new Singleton(); // 语句1
}
return INSTANCE; // 语句2
}
}
为什么多线程环境下,INSTANCE容易被多次实例化?
举例来说,假设INSTANCE尚未实例化的情况下,有线程A和线程B在同一时段访问getInstance()方法,
第1步:线程A执行语句1,此时线程A获得一个INSTANCE实例,设为x,即INSTANCE==x
第2步:线程B执行语句1,此时线程B获得又一个新的INSTANCE实例,设为y,即INSTANCE==y
第3步:线程A执行语句2,此时 INSTANCE==y
问题:
- 线程A应该返回x而不是被替换
- INSTANCE是单例应该全局唯一,不能先是x,一会儿又变成y
- 线程越多,问题越严重
解决:
只能有起初的一个线程执行new Singleton()生成实例, 所有线程都引用该实例
2.1 延迟加载 适用并发场景: synchronized方法
public class Singleton {
private static Singleton INSTANCE = null;
private Singleton() {
}
// 问题:synchronized属于重量级锁,锁住整个方法(影响范围大)会显著降低并发性能
public static synchronized Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
2.2 延迟加载 改善并发性能 : 双重检测
public class Singleton {
private static Singleton INSTANCE = null;
private Singleton() {
}
// 双重检测
// 问题:JVM 指令重排
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized(Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
什么是JVM指令重排?
new Singleton()在JVM执行时将其分为三个步骤(三条指令):
memory = allocate(); // 1. 分配对象的内存空间
ctorInstance(memory); // 2. 初始化对象
instance = memory; // 3. 设置instance指向刚分配的内存地址
编译器对指令执行顺序进行优化,改变顺序,例如上述2和3执行顺序互换:
memory = allocate(); // 1. 分配对象的内存空间
instance = memory; // 3. 设置instance指向刚分配的内存地址
ctorInstance(memory); // 2. 初始化对象
还未初始化对象,就将instance指向分配给对象的地址,带入到上一个singleton静态方法中观察:
public class Singleton {
private static Singleton INSTANCE = null;
private Singleton() {
}
public static Singleton getInstance() {
if (INSTANCE == null) { // 语句1
synchronized(Singleton.class) {
if (INSTANCE == null) {
// new Singleton() 被执行时细分为如下三步
/*memory = allocate();
instance = memory;
ctorInstance(memory); */
INSTANCE = new Singleton(); // 语句2
}
}
}
return INSTANCE;
}
}
线程A执行语句2: 执行完了 instance = memory; 尚未执行 ctorInstance(memory);
此时线程B执行语句1, 发现 INSTANCE不为空, 故直接返回, 此时INSTANCE是错误的
2.3 延迟加载 双重检测 同时禁用JVM指令重排: volatile关键字
public class Singleton {
// volatile 加这里
private static volatile Singleton INSTANCE = null;
private Singleton() {
}
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized(Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
总结: 能用直接加载还是别用懒加载了