每个 Java 开发者都曾被多线程环境下的单例问题困扰过。明明只想创建一个实例,却因为并发操作导致对象被重复创建,debug 半天才发现问题。传统的加锁方案虽然安全,但会带来不小的性能开销,那么有没有既安全又高效的方案呢?
单例模式的线程安全挑战
单例模式在系统中确保一个类只有唯一实例,并提供全局访问点。然而在多线程环境下,常见的懒汉式实现会遇到线程安全问题:
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
// 多线程环境下的危险区域
instance = new Singleton();
}
return instance;
}
}
上面的代码在多线程访问时,可能出现多个线程同时判断instance == null为真,导致创建多个实例。
最常见的解决方案是使用synchronized修饰获取实例的方法:
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
但这种方式有明显缺点:每次调用都要获取锁,即使单例已经初始化完成,仍然要经过锁的开销,严重影响性能。
接下来,我将介绍几种无锁或几乎无锁的线程安全单例实现方式。
静态内部类(Holder 模式)实现单例
静态内部类方式可能是最优雅的懒加载单例实现方式,它利用了 Java 类加载机制的线程安全特性:
public class LazyInitSingleton implements Serializable {
private static final long serialVersionUID = 1234567L;
// 私有构造函数,防御反射攻击
private LazyInitSingleton() {
if (SingletonHolder.INSTANCE != null) {
throw new IllegalStateException("单例已经初始化,不允许创建多个实例");
}
}
// 可以添加带参数的构造方法
private LazyInitSingleton(Config config) {
// 防御性检查避免NPE
if (config == null) {
throw new IllegalArgumentException("Config不能为null");
}
this.someConfig = config.getValue();
}
private String someConfig;
// 静态内部类,只有在使用时才会被加载
private static class SingletonHolder {
// 初始化时可以传入参数
private static final LazyInitSingleton INSTANCE = new LazyInitSingleton(Config.getDefaultConfig());
}
public static LazyInitSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
// 解决序列化问题的关键方法
private Object readResolve() {
// 返回单例实例,避免反序列化创建新对象
return SingletonHolder.INSTANCE;
}
}
这种实现的原理可以用下面的流程图解释:

工作原理
- 外部类 LazyInitSingleton 加载时,内部类 SingletonHolder 不会立即被加载
- 只有首次调用 getInstance()方法时,JVM 才会加载 SingletonHolder 类
- 类的加载和初始化过程是线程安全的,由 JVM 保证
- 静态变量 INSTANCE 只会初始化一次
举个简单例子:想象一下,你有一个精致的手表,但不想随身携带。你把它放在家里的保险柜中,只有当你需要查看时间时才会打开保险柜拿出手表。这就像静态内部类实现的单例——实例(手表)只有在第一次需要时才会被创建(从保险柜取出)。
序列化安全问题
静态内部类方式默认不是序列化安全的。如果单例类实现了 Serializable 接口,反序列化时会创建新实例,破坏单例性。解决方法是:
- 必须实现 Serializable 接口(如示例中所示)
- 添加 readResolve()方法,确保反序列化时返回单例实例
- 定义 serialVersionUID 避免兼容性问题
这种方式实现了真正的延迟加载(类加载时机由 JVM 控制),保证了线程安全,而且没有使用锁,性能非常好。
枚举实现单例
枚举可能是实现单例最简单的方式,由 Java 语言规范保证枚举实例的唯一性:
// 枚举单例可以实现接口
public enum EnumSingleton implements Initializable, AutoCloseable {
INSTANCE;
// 可以添加属性和方法
private String data;
private Connection dbConnection;
// 枚举构造函数会在类加载时执行一次
EnumSingleton() {
// 初始化资源
System.out.println("枚举单例初始化");
// 实际项目中可能的数据库连接初始化示例:
// try {
// dbConnection = DriverManager.getConnection(
// "jdbc:mysql://localhost:3306/mydb", "user", "password");
// } catch (SQLException e) {
// throw new RuntimeException("数据库连接失败", e);
// }
}
// 实现接口的初始化方法
@Override
public void initialize() {
System.out.println("单例初始化完成");
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
// 业务方法
public void doSomething() {
System.out.println("单例正在执行业务逻辑");
}
// 实现AutoCloseable接口,优雅关闭资源
@Override
public void close() {
// 关闭资源
if (dbConnection != null) {
try {
dbConnection.close();
} catch (Exception e) {
// 处理异常
}
}
}
}
使用方式非常简单:
// 获取实例
EnumSingleton singleton = EnumSingleton.INSTANCE;
// 调用方法
singleton.doSomething();
// 初始化
singleton.initialize();
// 使用try-with-resources自动关闭资源
try (EnumSingleton singleton = EnumSingleton.INSTANCE) {
singleton.doSomething();
}
枚举实现单例的优势:
- 代码简洁,实现最少
- 线程安全由 JVM 保证
- 自动防止反序列化创建新实例
- 防止反射攻击(JVM 禁止反射创建枚举实例)
- 可以实现接口,具有良好的扩展性
《Effective Java》第三版中,Joshua Bloch 明确指出:"单元素枚举类型是实现单例的最佳方式",因为它不仅能解决线程安全问题,还能防止反序列化和反射攻击。
注意:枚举单例在类加载时就会初始化(与饿汉式类似),不提供延迟加载能力。如果初始化成本高,且不一定会使用单例,可能会造成资源浪费。这与静态内部类最大的区别是初始化时机。
就像你家里的应急灯一样,它在你家里通电的那一刻就已经充电待命了,不管你是否真的会遇到停电情况。枚举单例也是如此,它在程序启动时就已经准备好了,不管你是否会用到它。
使用 CAS 操作实现单例
Compare-And-Swap(比较并交换)是一种无锁算法,可以通过 CPU 的原子指令来实现线程安全的单例:
import java.util.concurrent.atomic.AtomicReference;
public class CASSingleton {
// AtomicReference内部使用硬件级别的原子指令实现CAS
private static final AtomicReference<CASSingleton> INSTANCE =
new AtomicReference<>();
private CASSingleton() {}
// 优化版本:减少实例创建次数
public static CASSingleton getInstance() {
// 首先检查是否已有实例
CASSingleton current = INSTANCE.get();
if (current != null) {
return current;
}
// 只有实例不存在时才创建新实例
CASSingleton newInstance = new CASSingleton();
if (INSTANCE.compareAndSet(null, newInstance)) {
// CAS成功,返回新创建的实例
return newInstance;
} else {
// CAS失败,说明其他线程已经设置了实例
// 丢弃我们创建的实例
return INSTANCE.get();
}
}
}
CAS 操作的原理流程:

想象一下,这就像两个人同时尝试在空白纸上写下自己的名字。只有第一个人能成功在纸上写下名字(CAS 成功),第二个人看到纸上已经有名字了(CAS 失败),就会放弃写自己的名字,而是记住纸上写的那个名字。
这种方式有几个特点:
- 完全无锁,使用 CPU 硬件级别的原子指令保证线程安全
- 实现了延迟加载
- 优化版本减少了不必要的对象创建
- 在极高并发场景下性能优于细粒度锁方案
CAS 底层原理
CAS 操作依赖处理器的原子指令,如 x86 架构的CMPXCHG指令,能够在单个 CPU 周期内完成"比较并交换"操作。不同于锁的阻塞机制,CAS 是非阻塞的,线程可以立即得到结果并决定下一步操作。
然而,高并发情况下多线程同时执行 CAS 可能导致"总线风暴",因为每个 CPU 核心都会争夺内存总线访问权,影响整体性能。Java 的AtomicReference通过 Unsafe 类映射到这些硬件指令。
要验证 CAS 的效果,可以通过 JVM 参数-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly查看实际的汇编代码,其中会看到cmpxchg等指令的使用。
性能测试数据
在一项针对 100 万次调用的测试中,CAS 方式在高并发环境下的性能表现:

虽然 CAS 在线程数增加时性能有所下降,但仍明显优于同步锁方式。
双重检查锁+volatile 的优化
双重检查锁使用细粒度锁方案,虽然不是完全无锁,但它将锁的使用范围降到最小:
public class DCLSingleton {
// volatile关键字确保了:
// 1. 可见性:所有线程看到的instance值是最新的
// 2. 禁止指令重排序(禁止JVM重排序):防止构造函数还未执行完就返回实例
private static volatile DCLSingleton instance;
private DCLSingleton() {}
public static DCLSingleton getInstance() {
// 第一次检查:无锁检查,大多数情况下可以避免加锁
if (instance == null) {
synchronized (DCLSingleton.class) {
// 第二次检查:防止多个线程同时通过第一次检查
if (instance == null) {
instance = new DCLSingleton();
}
}
}
return instance;
}
}
就像排队买票,第一次检查相当于远远看一眼售票窗口是否有人,如果已经有人在那里工作(实例已创建),你就不用排队了(避免加锁)。只有当你看到没人(实例为 null)时,才会去排队(获取锁),排到你时再确认一下确实没人(再次检查实例是否为 null),才会创建一个新的售票员(创建实例)。
这种实现的关键是volatile关键字,它确保了:
- 可见性:一个线程的修改对其他线程立即可见
- 禁止指令重排序:防止实例创建过程中的重排序问题
指令重排序问题解析
instance = new DCLSingleton();这行代码实际上包含三个操作:
- 分配内存空间
- 调用构造函数初始化对象
- 将引用指向分配的内存
在没有 volatile 修饰的情况下,JVM 可能会优化指令顺序,导致步骤 3 在步骤 2 之前执行:

这就像给别人一个房子钥匙,但房子内部装修还没完成一样。如果没有 volatile 禁止这种重排序,其他线程可能会拿到钥匙(引用非空)进入一个还没装修好的房子(未初始化的对象)。
从 Java 5 开始,volatile 关键字提供了"happens-before"保证,确保对 volatile 变量的写操作会在读操作之前完成,从而防止上述问题。
双重检查的执行流程:

这种方式只有在首次创建实例时才使用锁,后续访问不再获取锁,因此性能损失很小。
防御反射和序列化攻击
即使实现了线程安全的单例,仍然可能面临反射和序列化的攻击:
反射攻击示例
// 攻击代码:使用反射破坏单例
try {
Constructor<LazyInitSingleton> constructor =
LazyInitSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
LazyInitSingleton instance1 = LazyInitSingleton.getInstance();
LazyInitSingleton instance2 = constructor.newInstance();
System.out.println(instance1 == instance2); // 输出false,单例被破坏
} catch (Exception e) {
e.printStackTrace();
}
就像有人知道了保险柜的密码(反射),即使你设置了访问限制(私有构造函数),他们仍然可以打开它并拿出手表的复制品。
序列化攻击示例
// 攻击代码:使用序列化破坏单例
try {
LazyInitSingleton instance1 = LazyInitSingleton.getInstance();
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(instance1);
oos.close();
// 反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
LazyInitSingleton instance2 = (LazyInitSingleton) ois.readObject();
ois.close();
System.out.println(instance1 == instance2); // 如果没有readResolve方法,输出false
} catch (Exception e) {
e.printStackTrace();
}
这就像复印了保险柜和手表的完整信息(序列化),然后根据这些信息在另一个地方重建了一个相同的保险柜和手表(反序列化)。
防御方法:
- 枚举单例:天然防御反射和序列化攻击
- 静态内部类:必须添加readResolve()方法防御序列化攻击,构造函数可添加检查防御反射攻击
- 构造函数检查:在私有构造函数中添加判断,防止反射创建多个实例
实际应用场景分析
根据不同的业务场景,我们可以选择最合适的单例实现方式:
场景 1:配置管理器
当你需要一个全局配置管理器,而且配置数据只需要加载一次时:
// 使用静态内部类实现的配置管理器
public class ConfigManager implements Serializable {
private static final long serialVersionUID = 7654321L;
private Map<String, String> configs;
private ConfigManager() {
// 初始化配置
configs = new HashMap<>();
configs.put("dbUrl", "jdbc:mysql://localhost:3306/mydb");
configs.put("maxConnections", "100");
}
// 允许通过配置文件初始化
private ConfigManager(String configPath) {
configs = new HashMap<>();
// 从指定路径加载配置
loadFromFile(configPath);
}
private void loadFromFile(String path) {
// 实际加载配置的代码
}
private static class Holder {
// 使用默认配置初始化
private static final ConfigManager INSTANCE = new ConfigManager();
}
public static ConfigManager getInstance() {
return Holder.INSTANCE;
}
public String getConfig(String key) {
return configs.get(key);
}
// 防止反序列化创建新实例
private Object readResolve() {
return Holder.INSTANCE;
}
}
场景 2:线程池管理器
当你需要一个全局的线程池管理器,且希望防止反序列化和反射攻击时:
// 使用枚举实现的线程池管理器
public enum ThreadPoolManager {
INSTANCE;
private final ExecutorService executorService;
private final ScheduledExecutorService scheduledExecutorService;
ThreadPoolManager() {
// 根据CPU核心数创建固定大小线程池
int processors = Runtime.getRuntime().availableProcessors();
executorService = new ThreadPoolExecutor(
processors, // 核心线程数,保持活跃的线程数
processors * 2, // 最大线程数,允许的最大线程数量
60L, TimeUnit.SECONDS, // 空闲线程存活时间
new LinkedBlockingQueue<>(), // 工作队列,存储等待执行的任务
new ThreadFactory() { // 自定义线程工厂
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("worker-" + t.getId());
t.setDaemon(true); // 设置为守护线程
return t;
}
},
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:调用者线程执行
);
// 创建调度线程池
scheduledExecutorService = Executors.newScheduledThreadPool(2);
}
public void execute(Runnable task) {
executorService.execute(task);
}
public Future<?> submit(Callable<?> task) {
return executorService.submit(task);
}
public ScheduledFuture<?> scheduleTask(Runnable task, long delay, TimeUnit unit) {
return scheduledExecutorService.schedule(task, delay, unit);
}
public void shutdown() {
executorService.shutdown();
scheduledExecutorService.shutdown();
}
}
场景 3:数据库连接管理
对于需要延迟初始化且初始化过程较重的场景:
// 使用双重检查实现的数据库连接管理器
public class DBConnectionManager implements AutoCloseable {
private static volatile DBConnectionManager instance;
private DataSource dataSource;
private DBConnectionManager() {
try {
// 初始化数据源
initDataSource();
} catch (Exception e) {
throw new RuntimeException("Failed to initialize data source", e);
}
}
private void initDataSource() {
// 使用连接池而不是单一连接
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
config.setUsername("user");
config.setPassword("password");
config.setMaximumPoolSize(10); // 最大连接数
config.setMinimumIdle(5); // 最小空闲连接
dataSource = new HikariDataSource(config);
}
public static DBConnectionManager getInstance() {
if (instance == null) {
synchronized (DBConnectionManager.class) {
if (instance == null) {
instance = new DBConnectionManager();
}
}
}
return instance;
}
public Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
@Override
public void close() {
if (dataSource instanceof HikariDataSource) {
((HikariDataSource) dataSource).close();
}
}
}
// 依赖项(Maven):
// <dependency>
// <groupId>com.zaxxer</groupId>
// <artifactId>HikariCP</artifactId>
// <version>4.0.3</version>
// </dependency>
场景 4:分布式系统中的单例
在分布式环境中,单例只能保证单个 JVM 内的唯一性,要实现全局唯一需要额外机制:
// 使用Redis分布式锁实现的分布式单例
public class DistributedSingleton {
private static volatile DistributedSingleton instance;
private static RedissonClient redissonClient;
private static final int MAX_RETRIES = 3;
static {
// 初始化Redisson客户端
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
redissonClient = Redisson.create(config);
}
private DistributedSingleton() {}
public static DistributedSingleton getInstance() {
if (instance == null) {
// 获取分布式锁
RLock lock = redissonClient.getLock("singleton_lock");
boolean lockAcquired = false;
try {
// 尝试获取锁,最多等待5秒,锁有效期10秒
lockAcquired = lock.tryLock(5, 10, TimeUnit.SECONDS);
if (lockAcquired) {
if (instance == null) {
instance = new DistributedSingleton();
}
} else {
// 获取锁失败,可能是高并发或网络问题
// 重试或返回已有实例
if (instance != null) {
return instance;
}
// 重试策略
return retryGetInstance(MAX_RETRIES);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lockAcquired) {
lock.unlock();
}
}
}
return instance;
}
private static DistributedSingleton retryGetInstance(int retries) {
if (retries <= 0) {
throw new RuntimeException("获取分布式单例实例超过最大重试次数");
}
try {
Thread.sleep(100);
return getInstance();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return retryGetInstance(retries - 1);
}
}
}
// 依赖项(Maven):
// <dependency>
// <groupId>org.redisson</groupId>
// <artifactId>redisson</artifactId>
// <version>3.17.0</version>
// </dependency>
分布式单例就像是多个城市中的唯一银行金库,每个城市(JVM)内部可以有本地的管理者(本地单例),但要确保全球(分布式系统)只有一个主金库,就需要一个中央协调机构(Redis)来控制。
单例模式与依赖注入
在现代应用架构中,单例模式与依赖注入(DI)框架有一定的关系:
// Spring框架中的单例Bean
@Component
public class SpringSingleton {
// Spring默认为单例作用域
private String data;
// 通过构造函数注入依赖
@Autowired
public SpringSingleton(DataService dataService) {
this.data = dataService.loadData();
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}
// 使用方式
@Service
public class UserService {
private final SpringSingleton singleton;
@Autowired
public UserService(SpringSingleton singleton) {
this.singleton = singleton;
}
}
使用 DI 框架的优势:
- 生命周期管理更灵活(可配置作用域)
- 便于测试(可以注入模拟对象)
- 更容易实现面向接口编程
- 自动管理依赖关系
Spring 容器负责确保单例的唯一性,同时提供了额外功能如懒加载(@Lazy注解)和作用域控制。
单例模式的单元测试
测试单例类时,一个常见问题是如何隔离测试环境:
// 使用可测试的单例模式
public class TestableSingleton {
private static TestableSingleton instance;
// 用于测试的重置方法
static void reset() {
instance = null;
}
private TestableSingleton() {}
public static TestableSingleton getInstance() {
if (instance == null) {
synchronized (TestableSingleton.class) {
if (instance == null) {
instance = new TestableSingleton();
}
}
}
return instance;
}
}
// JUnit测试代码
public class TestableSingletonTest {
@Before
public void setup() {
TestableSingleton.reset();
}
@Test
public void testSingletonInstance() {
TestableSingleton instance1 = TestableSingleton.getInstance();
TestableSingleton instance2 = TestableSingleton.getInstance();
assertSame("单例实例应该相同", instance1, instance2);
}
@Test
public void testMultithreadedAccess() throws Exception {
// 多线程测试
final int threadCount = 10;
final CountDownLatch latch = new CountDownLatch(threadCount);
final Set<TestableSingleton> instances = Collections.synchronizedSet(new HashSet<>());
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
instances.add(TestableSingleton.getInstance());
latch.countDown();
}).start();
}
latch.await(1, TimeUnit.SECONDS);
assertEquals("多线程下应该只有一个单例实例", 1, instances.size());
}
}
各实现方式性能对比
以下是不同实现方式在各种并发级别下的性能测试结果(每秒处理请求数):

从数据可以看出,枚举和静态内部类在各种并发级别下性能最好,且随着并发增加性能衰减较少。CAS 方法在高并发下性能下降较明显,但仍优于使用锁的方法。这就像不同道路在交通流量增加时的通行效率一样。
如何选择最佳实现方式
下面的决策流程图可以帮助你选择合适的单例实现方式:

总结
|
实现方式 |
线程安全 |
延迟加载 |
防反射攻击 |
防反序列化 |
性能(百万次/秒) |
实现复杂度 |
JDK 要求 |
适用场景 |
|
静态内部类 |
✓ |
✓ |
✗(可添加) |
✗(需添加方法) |
30-37 |
低 |
1.2+ |
大多数场景,尤其是需要延迟加载的情况 |
|
枚举方式 |
✓ |
✗ |
✓ |
✓ |
32-38 |
最低 |
1.5+ |
需要防止反射和序列化攻击的场景 |
|
CAS 方式 |
✓ |
✓ |
✗ |
✗ |
22-35 |
中 |
1.5+ |
极高并发且对锁敏感的场景 |
|
双重检查锁 |
✓ |
✓ |
✗ |
✗ |
20-34 |
中 |
1.5+ |
需要延迟加载且初始化过程复杂的场景 |
|
分布式实现 |
✓ |
视情况 |
✗ |
✗ |
<10 |
高 |
视情况 |
分布式系统中需要全局唯一的场景 |
无锁实现单例的方式各有优劣,选择时需要考虑以下因素:
- 是否需要延迟加载
- 初始化成本有多高
- 是否需要防止反射和序列化攻击
- 系统的并发级别
- 是否为分布式环境
在大多数场景下,静态内部类和枚举方式是最佳选择。在选择时应根据业务场景的具体需求进行权衡,优先考虑代码简洁性和可维护性。
真正的高手不仅知道各种实现方式,更懂得在什么场景下选择最合适的方案。就像一个好厨师不仅了解各种食材,更知道什么菜该用什么食材一样。
1137

被折叠的 条评论
为什么被折叠?



