在Java多线程程序中,懒加载指的是在真正需要某个对象时才去创建它,从而节省系统资源。但在并发场景下,如果多个线程同时判断对象为空并去初始化,就可能创建出多个实例或读取到未完全构造的对象。为了解决这个问题,可以使用同步方法和双重校验锁这两种常见方案。

一、使用同步方法实现线程安全懒加载
最简单的方式是将获取实例的方法声明为 synchronized,保证同一时刻只有一个线程能进入方法体。这种方式正确性高,但每次调用都会加锁,性能开销较大。
public class SingletonSync {
private static SingletonSync instance;
// 同步方法保证线程安全
public static synchronized SingletonSync getInstance() {
if (instance == null) {
instance = new SingletonSync();
}
return instance;
}
}
二、双重校验锁方案
双重校验锁在第一次判断为空后才进入同步块,并在同步块内再次判断。配合 volatile 关键字禁止指令重排序,既能减少锁竞争,又能保证线程安全。
public class SingletonDCL {
// volatile防止指令重排序导致其他线程拿到未初始化对象
private static volatile SingletonDCL instance;
public static SingletonDCL getInstance() {
if (instance == null) { // 第一次校验,避免不必要的加锁
synchronized (SingletonDCL.class) {
if (instance == null) { // 第二次校验,防止重复创建
instance = new SingletonDCL();
}
}
}
return instance;
}
}
为什么需要 volatile
对象创建并非原子操作,包含分配内存、执行构造、赋值引用三步。若无 volatile,可能发生指令重排,使线程读到不为 null 但还未构造完成的对象,从而引发错误。
三、两种方案对比
| 方案 | 线程安全 | 性能 | 实现复杂度 |
|---|---|---|---|
| 同步方法 | 安全 | 较低 | 简单 |
| 双重校验锁 | 安全 | 较高 | 中等 |
四、注意事项
- 不要省略第二次空判断,否则在锁外等待的线程可能再次创建实例。
- 使用
volatile修饰实例变量是双重校验锁正确性的关键。 - 若使用静态内部类或枚举实现懒加载,也能天然保证线程安全且更简洁。
在绝大多数业务场景中,双重校验锁是平衡性能与安全性的优选方案,但需严格遵循 volatile 与两次判断的写法。