Java单例模式要求一个类在全局仅存在一个实例,在多线程并发访问的场景下,如果实现方式不当,就可能出现多个线程同时创建实例的竞态条件,同时单例内部共享数据的并发操作也会引发数据一致性问题。这些问题轻则导致程序逻辑异常,重则引发线上故障,因此掌握单例模式下的并发安全保障方法非常重要。

单例模式中竞态条件的产生原因
竞态条件指的是多个线程同时访问共享资源,并且访问顺序会影响程序执行结果的情况。在单例模式的懒加载实现中,常见的非线程安全写法如下:
public class UnsafeSingleton {
private static UnsafeSingleton instance;
private int count; // 共享计数变量
private UnsafeSingleton() {
count = 0;
}
public static UnsafeSingleton getInstance() {
if (instance == null) {
// 多个线程可能同时进入该分支,创建多个实例
instance = new UnsafeSingleton();
}
return instance;
}
public void incrementCount() {
count++; // 非原子操作,也会引发竞态条件
}
public int getCount() {
return count;
}
}
上述代码中,getInstance()方法在多线程同时调用时,可能有两个线程同时判断instance == null成立,进而分别创建实例,破坏了单例的唯一性。同时incrementCount()方法中的count++不是原子操作,会拆分为读取、加一、写入三个步骤,多线程并发执行时会出现数据丢失的问题。
线程安全的单例实现方式
1. 饿汉式单例
饿汉式单例在类加载阶段就完成实例的初始化,借助类加载机制保证线程安全,实现简单但无法实现懒加载:
public class HungrySingleton {
// 类加载时直接初始化实例
private static final HungrySingleton instance = new HungrySingleton();
private int count;
private HungrySingleton() {
count = 0;
}
public static HungrySingleton getInstance() {
return instance;
}
public synchronized void incrementCount() {
count++;
}
public int getCount() {
return count;
}
}
这种方式不会出现实例创建的竞态条件,但如果单例初始化成本较高且程序全程未使用该实例,会造成资源浪费。
2. 双重检查锁定单例
双重检查锁定既可以实现懒加载,又能保证线程安全,同时避免了每次获取实例都加锁的性能开销:
public class DoubleCheckSingleton {
// 使用volatile保证可见性和禁止指令重排
private static volatile DoubleCheckSingleton instance;
private int count;
private DoubleCheckSingleton() {
count = 0;
}
public static DoubleCheckSingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckSingleton.class) {
if (instance == null) {
instance = new DoubleCheckSingleton();
}
}
}
return instance;
}
public void incrementCount() {
synchronized (this) {
count++;
}
}
public int getCount() {
return count;
}
}
这里instance变量必须加volatile修饰,否则可能因为指令重排,导致其他线程拿到未完全初始化的实例。对共享数据count的修改加同步锁,保证操作的原子性。
3. 静态内部类单例
静态内部类单例是推荐的实现方式,既利用了类加载的线程安全特性,又实现了懒加载,代码更简洁:
public class InnerClassSingleton {
private int count;
private InnerClassSingleton() {
count = 0;
}
private static class SingletonHolder {
// 静态内部类加载时才初始化实例
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
public static InnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
public void incrementCount() {
synchronized (this) {
count++;
}
}
public int getCount() {
return count;
}
}
并发数据一致性的额外保障措施
除了保证单例实例的唯一性,还需要对单例内部的共享数据做并发控制:
- 对于计数、累加等场景,可以使用
AtomicInteger等原子类,避免手动加锁:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicSingleton {
private static volatile AtomicSingleton instance;
private AtomicInteger count = new AtomicInteger(0);
private AtomicSingleton() {}
public static AtomicSingleton getInstance() {
if (instance == null) {
synchronized (AtomicSingleton.class) {
if (instance == null) {
instance = new AtomicSingleton();
}
}
}
return instance;
}
public void incrementCount() {
count.incrementAndGet(); // 原子操作,无竞态条件
}
public int getCount() {
return count.get();
}
}
- 如果共享数据是只读的,可以在初始化时完成赋值,之后不再修改,天然保证线程安全。
- 对于复杂的共享数据操作,可以使用
ReentrantLock或者synchronized代码块,明确锁的范围,避免死锁。
实践建议总结
在实际开发中,优先选择静态内部类方式实现单例,既能保证线程安全又能实现懒加载。如果单例内部有共享的可变数据,需要根据场景选择同步锁或者原子类保障操作的一致性。避免使用没有做线程安全处理的懒加载单例写法,上线前可以通过多线程压测验证单例的唯一性和数据操作的正确性,提前规避竞态条件带来的问题。