引言:单例模式的核心挑战
单例模式(Singleton Pattern)作为创建型设计模式中最常用的一种,确保一个类仅有一个实例,并提供一个全局访问点。然而,在多线程环境下,单例模式的实现并非看起来那么简单。当多个线程同时尝试访问单例实例时,可能会遇到线程安全问题和资源竞争,导致程序行为异常甚至崩溃。
本文将深入探讨单例模式在实际开发中可能遇到的线程安全与资源竞争问题,并提供多种高效解决方案,帮助开发者在不同场景下选择最适合的实现方式。
一、单例模式的基本实现及其线程安全问题
1.1 懒汉式单例的线程安全隐患
最基础的懒汉式单例实现如下:
public class Singleton {
private static Singleton instance;
private Singleton() {
// 私有构造函数防止外部实例化
System.out.println("Singleton实例被创建");
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
问题分析:
在多线程环境下,假设线程A和线程B同时执行到if (instance == null)判断,此时instance确实为null,两个线程都会进入if块内部并尝试创建实例。这将导致:
- 实例被创建两次,违反单例原则
- 可能产生两个不同的实例对象
- 资源浪费和状态不一致
1.2 简单的同步方法解决及其性能问题
为了解决上述问题,最简单的方案是使用synchronized关键字:
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
性能问题: 虽然这种实现保证了线程安全,但每次获取实例都需要进行同步,即使实例已经创建。在高并发场景下,频繁的同步操作会成为性能瓶颈。
二、饿汉式单例:空间换时间
2.1 饿汉式实现
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
优点:
- 线程安全:实例在类加载时创建,由JVM保证线程安全
- 无同步开销:获取实例无需同步
缺点:
- 资源浪费:如果实例创建开销大但很少使用,会造成资源浪费
- 无法延迟加载:即使不需要实例,类加载时也会创建
三、双重检查锁定(Double-Checked Locking)及其陷阱
3.1 DCL实现(错误版本)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
问题分析:
这个实现存在指令重排序问题。instance = new Singleton()并非原子操作,它包含三个步骤:
- 分配内存空间
- 初始化对象
- 将instance指向分配的内存地址
如果JVM发生指令重排序,步骤2和3可能交换顺序。此时,线程A执行了步骤1和3(instance已指向内存地址但未初始化),线程B判断instance不为null直接返回,使用了一个未完全初始化的对象,导致严重错误。
3.2 DCL的正确实现(使用volatile)
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
关键点:
volatile关键字禁止指令重排序,保证instance完全初始化前不会被其他线程看到- 保证了线程安全和性能的平衡
四、静态内部类(Initialization-on-demand Holder)模式
4.1 实现方式
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
原理分析:
- SingletonHolder是一个静态内部类,只有在getInstance()方法被调用时才会加载
- JVM的类加载机制保证了静态变量初始化的线程安全性
- 实现了延迟加载,且无同步开销
五、枚举单例:最安全的实现方式
5.1 枚举实现
public enum Singleton {
INSTANCE;
private int value;
public void setValue(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public void doSomething() {
System.out.println("执行业务逻辑,value=" + value);
}
}
使用方式:
public class Main {
public static void main(String[] args) {
// 获取单例实例
Singleton singleton = Singleton.INSTANCE;
singleton.setValue(100);
singleton.doSomething();
// 验证单例
Singleton another = Singleton.INSTANCE;
System.out.println(singleton == another); // true
}
}
优势:
- 由JVM保证线程安全
- 防止反射攻击(反射创建枚举实例会抛出异常)
- 防止序列化问题(枚举默认实现了序列化接口)
- 代码简洁
六、ThreadLocal实现:线程隔离单例
6.1 ThreadLocal单例实现
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance =
ThreadLocal.withInitial(() -> new ThreadLocalSingleton());
private ThreadLocalSingleton() {}
public static ThreadLocalSingleton getInstance() {
return threadLocalSingleton.get();
}
public static void removeInstance() {
threadLocalInstance.remove();
}
}
适用场景:
- 每个线程需要独立的单例实例
- 例如:数据库连接、事务管理等
七、实际开发中的资源竞争问题及解决方案
7.1 单例实例初始化时的资源竞争
场景:单例实例初始化需要访问外部资源(如数据库、文件系统),多个线程同时触发初始化可能导致资源竞争。
解决方案:
public class ResourceSingleton {
private static volatile ResourceSingleton instance;
private final Connection connection;
private final AtomicBoolean initialized = new AtomicBoolean(false);
private ResourceSingleton() {
// 模拟耗时的资源初始化
this.connection = createDatabaseConnection();
initialized.set(true);
}
public static ResourceSingleton getInstance() {
if (instance == null) {
synchronized (ResourceSingleton.class) {
if (instance == null) {
instance = new ResourceSingleton();
}
}
}
return instance;
}
private Connection createDatabaseConnection() {
// 模拟数据库连接创建
try {
Thread.sleep(100); // 模拟耗时操作
return DriverManager.getConnection("jdbc:mysql://localhost:3306/test");
} catch (Exception e) {
throw new RuntimeException("数据库连接失败", e);
}
}
public void executeQuery(String sql) {
if (!initialized.get()) {
throw new IllegalStateException("实例未初始化完成");
}
// 执行查询逻辑
}
}
7.2 单例实例状态竞争
场景:单例实例包含可变状态,多个线程同时修改可能导致状态不一致。
解决方案:
public class StatefulSingleton {
private static final StatefulSingleton INSTANCE = new StatefulSingleton();
private final Map<String, Object> cache = new ConcurrentHashMap<>();
private final AtomicLong counter = new AtomicLong(0);
private StatefulSingleton() {}
public static StatefulSingleton getInstance() {
return INSTANCE;
}
// 使用ConcurrentHashMap保证并发访问安全
public void putCache(String key, Object value) {
cache.put(key, value);
}
public Object getCache(String key) {
return cache.get(key);
}
// 使用AtomicLong保证原子操作
public long incrementCounter() {
return counter.incrementAndGet();
}
public long getCounter() {
return counter.get();
}
}
八、性能对比与选择建议
8.1 性能对比测试
public class SingletonPerformanceTest {
private static final int THREAD_COUNT = 100;
private static final int INVOCATIONS_PER_THREAD = 10000;
public static void test饿汉式() throws InterruptedException {
long start = System.currentTimeMillis();
CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
for (int j = 0; j < INVOCATIONS_PER_THREAD; j++) {
EagerSingleton.getInstance();
}
latch.countDown();
}).start();
}
latch.await();
long end = System.currentTimeMillis();
System.out.println("饿汉式耗时: " + (end - start) + "ms");
}
public static void testDCL() throws InterruptedException {
long start = System.currentTimeMillis();
CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
for (int j = 0; j < INVOCATIONS_PER_THREAD; j++) {
DCLSingleton.getInstance();
}
latch.countDown();
}).start();
}
latch.await();
long end = System.currentTimeMillis();
System.out.println("DCL耗时: " + (end - start) + "ms");
}
public static void main(String[] args) throws InterruptedException {
test饿汉式();
testDCL();
}
}
测试结果分析:
- 饿汉式:无同步开销,性能最优
- DCL:首次访问有同步开销,后续访问无开销,性能接近饿汉式
- 同步方法:每次访问都有同步开销,性能最差
8.2 选择建议
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 实例创建开销小,频繁使用 | 饿汉式 | 简单高效,无同步开销 |
| 实例创建开销大,需要延迟加载 | DCL或静态内部类 | 平衡性能与延迟加载 |
| 需要防止反射/序列化攻击 | 枚举 | 最安全,JVM保证 |
| 需要线程隔离实例 | ThreadLocal | 满足线程隔离需求 |
| 高并发场景 | 静态内部类或枚举 | 无同步开销,性能最优 |
九、高级主题:反射攻击与防御
9.1 反射攻击示例
public class ReflectionAttack {
public static void main(String[] args) throws Exception {
// 获取私有构造器
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
// 尝试创建多个实例
Singleton instance1 = constructor.newInstance();
Singleton instance2 = constructor.newInstance();
System.out.println(instance1 == instance2); // false,单例被破坏
}
}
9.2 防御方案
public class SecureSingleton {
private static volatile SecureSingleton instance;
private static boolean flag = false;
private SecureSingleton() {
// 防御反射攻击
if (flag) {
throw new RuntimeException("单例模式禁止反射调用");
}
flag = true;
}
public static SecureSingleton getInstance() {
if (instance == null) {
synchronized (SecureSingleton.class) {
if (instance == null) {
instance = new SecureSingleton();
}
}
}
return instance;
}
}
十、总结
单例模式在多线程环境下的实现需要考虑线程安全、资源竞争、性能等多个维度。不同实现方式各有优劣:
- 饿汉式:简单高效,适用于实例创建开销小的场景
- 懒汉式(同步方法):线程安全但性能差,不推荐
- 双重检查锁定(DCL):性能与延迟加载的平衡,需注意volatile的使用
- 静态内部类:推荐方案,兼顾延迟加载、线程安全和性能
- 枚举:最安全的方案,防止反射和序列化攻击
- ThreadLocal:适用于线程隔离场景
在实际开发中,应根据具体需求选择合适的实现方式,并注意处理资源竞争、状态一致性等潜在问题。同时,考虑使用枚举或静态内部类等更安全、更简洁的实现方式,避免过度设计带来的复杂性。# 深入解析调用单例模式方法在实际开发中可能遇到的线程安全与资源竞争问题及高效解决方案
引言:单例模式的核心挑战
单例模式(Singleton Pattern)作为创建型设计模式中最常用的一种,确保一个类仅有一个实例,并提供一个全局访问点。然而,在多线程环境下,单例模式的实现并非看起来那么简单。当多个线程同时尝试访问单例实例时,可能会遇到线程安全问题和资源竞争,导致程序行为异常甚至崩溃。
本文将深入探讨单例模式在实际开发中可能遇到的线程安全与资源竞争问题,并提供多种高效解决方案,帮助开发者在不同场景下选择最适合的实现方式。
一、单例模式的基本实现及其线程安全问题
1.1 懒汉式单例的线程安全隐患
最基础的懒汉式单例实现如下:
public class Singleton {
private static Singleton instance;
private Singleton() {
// 私有构造函数防止外部实例化
System.out.println("Singleton实例被创建");
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
问题分析:
在多线程环境下,假设线程A和线程B同时执行到if (instance == null)判断,此时instance确实为null,两个线程都会进入if块内部并尝试创建实例。这将导致:
- 实例被创建两次,违反单例原则
- 可能产生两个不同的实例对象
- 资源浪费和状态不一致
代码演示问题:
public class SingletonDemo {
public static void main(String[] args) throws InterruptedException {
// 创建10个线程同时获取单例实例
for (int i = 0; i < 10; i++) {
new Thread(() -> {
Singleton instance = Singleton.getInstance();
System.out.println(Thread.currentThread().getName() + "获取到实例: " + instance);
}).start();
}
Thread.sleep(1000);
}
}
运行结果(可能输出):
Thread-0获取到实例: com.example.Singleton@1a2b3c4d
Thread-1获取到实例: com.example.Singleton@5e6f7a8b
Thread-2获取到实例: com.example.Singleton@1a2b3c4d
可以看到创建了两个不同的实例对象。
1.2 简单的同步方法解决及其性能问题
为了解决上述问题,最简单的方案是使用synchronized关键字:
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
性能问题分析: 虽然这种实现保证了线程安全,但每次获取实例都需要进行同步,即使实例已经创建。在高并发场景下,频繁的同步操作会成为性能瓶颈。
性能测试代码:
public class PerformanceTest {
public static void main(String[] args) throws InterruptedException {
int threadCount = 100;
int invocationsPerThread = 10000;
// 测试同步方法版本
long start = System.currentTimeMillis();
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
for (int j = 0; j < invocationsPerThread; j++) {
SynchronizedSingleton.getInstance();
}
latch.countDown();
}).start();
}
latch.await();
long end = System.currentTimeMillis();
System.out.println("同步方法版本耗时: " + (end - start) + "ms");
}
}
测试结果:
- 同步方法版本:约 150-200ms(每次调用都有同步开销)
- 无同步版本:约 5-10ms(但线程不安全)
二、饿汉式单例:空间换时间
2.1 饿汉式实现
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
优点:
- 线程安全:实例在类加载时创建,由JVM保证线程安全
- 无同步开销:获取实例无需同步
缺点:
- 资源浪费:如果实例创建开销大但很少使用,会造成资源浪费
- 无法延迟加载:即使不需要实例,类加载时也会创建
适用场景:
// 适用于实例创建开销小且频繁使用的场景
public class ConfigManager {
private static final ConfigManager instance = new ConfigManager();
private final Properties config = new Properties();
private ConfigManager() {
// 加载配置文件
try {
config.load(getClass().getResourceAsStream("/config.properties"));
} catch (IOException e) {
throw new RuntimeException("配置加载失败", e);
}
}
public static ConfigManager getInstance() {
return instance;
}
public String getConfig(String key) {
return config.getProperty(key);
}
}
三、双重检查锁定(Double-Checked Locking)及其陷阱
3.1 DCL实现(错误版本)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
问题分析:
这个实现存在指令重排序问题。instance = new Singleton()并非原子操作,它包含三个步骤:
- 分配内存空间
- 初始化对象
- 将instance指向分配的内存地址
如果JVM发生指令重排序,步骤2和3可能交换顺序。此时,线程A执行了步骤1和3(instance已指向内存地址但未初始化),线程B判断instance不为null直接返回,使用了一个未完全初始化的对象,导致严重错误。
指令重排序演示:
public class ReorderingDemo {
private static boolean ready = false;
private static int value = 0;
public static void main(String[] args) throws InterruptedException {
Thread writer = new Thread(() -> {
value = 42; // 步骤1
ready = true; // 步骤2
});
Thread reader = new Thread(() -> {
while (!ready) { // 可能永远循环
Thread.yield();
}
System.out.println(value); // 可能输出0,而不是42
});
writer.start();
reader.start();
writer.join();
reader.join();
}
}
3.2 DCL的正确实现(使用volatile)
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
关键点:
volatile关键字禁止指令重排序,保证instance完全初始化前不会被其他线程看到- 保证了线程安全和性能的平衡
volatile的内存语义:
// volatile写操作:保证对volatile变量的写操作,happens-before于后续对这个变量的读操作
// volatile读操作:保证读取到最新值
public class VolatileExample {
private volatile int counter = 0;
public void increment() {
counter++; // 非原子操作,但volatile保证可见性
}
public int getCounter() {
return counter; // 总是读取最新值
}
}
四、静态内部类(Initialization-on-demand Holder)模式
4.1 实现方式
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
原理分析:
- SingletonHolder是一个静态内部类,只有在getInstance()方法被调用时才会加载
- JVM的类加载机制保证了静态变量初始化的线程安全性
- 实现了延迟加载,且无同步开销
类加载过程详解:
public class ClassLoadingDemo {
// 类加载的五个阶段:加载、验证、准备、解析、初始化
// 静态内部类在首次使用时才加载
private static class Holder {
// 准备阶段:为静态变量分配内存并设置默认值(0或null)
// 初始化阶段:执行静态变量的初始化和静态代码块
private static final ComplexObject INSTANCE = new ComplexObject();
static {
System.out.println("Holder类被初始化");
}
}
public static ComplexObject getInstance() {
// 首次调用时触发Holder类加载
return Holder.INSTANCE;
}
}
class ComplexObject {
private final int value;
public ComplexObject() {
// 模拟耗时初始化
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
this.value = 42;
System.out.println("ComplexObject实例创建完成");
}
}
五、枚举单例:最安全的实现方式
5.1 枚举实现
public enum Singleton {
INSTANCE;
private int value;
public void setValue(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public void doSomething() {
System.out.println("执行业务逻辑,value=" + value);
}
}
使用方式:
public class Main {
public static void main(String[] args) {
// 获取单例实例
Singleton singleton = Singleton.INSTANCE;
singleton.setValue(100);
singleton.doSomething();
// 验证单例
Singleton another = Singleton.INSTANCE;
System.out.println(singleton == another); // true
}
}
优势:
- 由JVM保证线程安全
- 防止反射攻击(反射创建枚举实例会抛出异常)
- 防止序列化问题(枚举默认实现了序列化接口)
- 代码简洁
反编译分析:
// 枚举编译后的字节码实际上相当于:
public final class Singleton extends Enum<Singleton> {
public static final Singleton INSTANCE;
private int value;
static {
INSTANCE = new Singleton("INSTANCE", 0);
// JVM保证线程安全
}
private Singleton(String name, int ordinal) {
super(name, ordinal);
}
}
六、ThreadLocal实现:线程隔离单例
6.1 ThreadLocal单例实现
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance =
ThreadLocal.withInitial(() -> new ThreadLocalSingleton());
private ThreadLocalSingleton() {}
public static ThreadLocalSingleton getInstance() {
return threadLocalInstance.get();
}
public static void removeInstance() {
threadLocalInstance.remove();
}
}
适用场景:
- 每个线程需要独立的单例实例
- 例如:数据库连接、事务管理、用户会话等
实际应用示例:
public class UserSessionManager {
private static final ThreadLocal<UserSession> currentUserSession = new ThreadLocal<>();
public static void setCurrentUser(UserSession session) {
currentUserSession.set(session);
}
public static UserSession getCurrentUser() {
return currentUserSession.get();
}
public static void clear() {
currentUserSession.remove();
}
}
// 在Web应用中使用
public class UserSessionFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
// 从请求中解析用户信息并设置到ThreadLocal
UserSession session = parseSessionFromRequest((HttpServletRequest) request);
UserSessionManager.setCurrentUser(session);
// 继续处理请求
chain.doFilter(request, response);
} finally {
// 确保清理,防止内存泄漏
UserSessionManager.clear();
}
}
}
七、实际开发中的资源竞争问题及解决方案
7.1 单例实例初始化时的资源竞争
场景:单例实例初始化需要访问外部资源(如数据库、文件系统),多个线程同时触发初始化可能导致资源竞争。
解决方案:
public class ResourceSingleton {
private static volatile ResourceSingleton instance;
private final Connection connection;
private final AtomicBoolean initialized = new AtomicBoolean(false);
private ResourceSingleton() {
// 模拟耗时的资源初始化
this.connection = createDatabaseConnection();
initialized.set(true);
}
public static ResourceSingleton getInstance() {
if (instance == null) {
synchronized (ResourceSingleton.class) {
if (instance == null) {
instance = new ResourceSingleton();
}
}
}
return instance;
}
private Connection createDatabaseConnection() {
// 模拟数据库连接创建
try {
Thread.sleep(100); // 模拟耗时操作
return DriverManager.getConnection("jdbc:mysql://localhost:3306/test");
} catch (Exception e) {
throw new RuntimeException("数据库连接失败", e);
}
}
public void executeQuery(String sql) {
if (!initialized.get()) {
throw new IllegalStateException("实例未初始化完成");
}
// 执行查询逻辑
}
}
优化方案:使用CompletableFuture异步初始化:
public class AsyncSingleton {
private static volatile AsyncSingleton instance;
private final CompletableFuture<Connection> connectionFuture;
private AsyncSingleton() {
this.connectionFuture = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(100);
return DriverManager.getConnection("jdbc:mysql://localhost:3306/test");
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
public static AsyncSingleton getInstance() {
if (instance == null) {
synchronized (AsyncSingleton.class) {
if (instance == null) {
instance = new AsyncSingleton();
}
}
}
return instance;
}
public Connection getConnection() {
try {
return connectionFuture.get(5, TimeUnit.SECONDS);
} catch (Exception e) {
throw new RuntimeException("获取连接失败", e);
}
}
}
7.2 单例实例状态竞争
场景:单例实例包含可变状态,多个线程同时修改可能导致状态不一致。
解决方案:
public class StatefulSingleton {
private static final StatefulSingleton INSTANCE = new StatefulSingleton();
private final Map<String, Object> cache = new ConcurrentHashMap<>();
private final AtomicLong counter = new AtomicLong(0);
private StatefulSingleton() {}
public static StatefulSingleton getInstance() {
return INSTANCE;
}
// 使用ConcurrentHashMap保证并发访问安全
public void putCache(String key, Object value) {
cache.put(key, value);
}
public Object getCache(String key) {
return cache.get(key);
}
// 使用AtomicLong保证原子操作
public long incrementCounter() {
return counter.incrementAndGet();
}
public long getCounter() {
return counter.get();
}
}
复杂状态管理示例:
public class ComplexStateSingleton {
private static final ComplexStateSingleton INSTANCE = new ComplexStateSingleton();
// 使用读写锁保护复杂状态
private final Map<String, List<String>> userRoles = new HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private ComplexStateSingleton() {}
public static ComplexStateSingleton getInstance() {
return INSTANCE;
}
public void addUserRole(String user, String role) {
lock.writeLock().lock();
try {
userRoles.computeIfAbsent(user, k -> new ArrayList<>()).add(role);
} finally {
lock.writeLock().unlock();
}
}
public List<String> getUserRoles(String user) {
lock.readLock().lock();
try {
return new ArrayList<>(userRoles.getOrDefault(user, Collections.emptyList()));
} finally {
lock.readLock().unlock();
}
}
}
八、性能对比与选择建议
8.1 性能对比测试
public class SingletonPerformanceTest {
private static final int THREAD_COUNT = 100;
private static final int INVOCATIONS_PER_THREAD = 10000;
public static void test饿汉式() throws InterruptedException {
long start = System.currentTimeMillis();
CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
for (int j = 0; j < INVOCATIONS_PER_THREAD; j++) {
EagerSingleton.getInstance();
}
latch.countDown();
}).start();
}
latch.await();
long end = System.currentTimeMillis();
System.out.println("饿汉式耗时: " + (end - start) + "ms");
}
public static void testDCL() throws InterruptedException {
long start = System.currentTimeMillis();
CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
for (int j = 0; j < INVOCATIONS_PER_THREAD; j++) {
DCLSingleton.getInstance();
}
latch.countDown();
}).start();
}
latch.await();
long end = System.currentTimeMillis();
System.out.println("DCL耗时: " + (end - start) + "ms");
}
public static void main(String[] args) throws InterruptedException {
test饿汉式();
testDCL();
}
}
测试结果分析:
- 饿汉式:无同步开销,性能最优
- DCL:首次访问有同步开销,后续访问无开销,性能接近饿汉式
- 同步方法:每次访问都有同步开销,性能最差
8.2 选择建议
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 实例创建开销小,频繁使用 | 饿汉式 | 简单高效,无同步开销 |
| 实例创建开销大,需要延迟加载 | DCL或静态内部类 | 平衡性能与延迟加载 |
| 需要防止反射/序列化攻击 | 枚举 | 最安全,JVM保证 |
| 需要线程隔离实例 | ThreadLocal | 满足线程隔离需求 |
| 高并发场景 | 静态内部类或枚举 | 无同步开销,性能最优 |
九、高级主题:反射攻击与防御
9.1 反射攻击示例
public class ReflectionAttack {
public static void main(String[] args) throws Exception {
// 获取私有构造器
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
// 尝试创建多个实例
Singleton instance1 = constructor.newInstance();
Singleton instance2 = constructor.newInstance();
System.out.println(instance1 == instance2); // false,单例被破坏
}
}
9.2 防御方案
public class SecureSingleton {
private static volatile SecureSingleton instance;
private static boolean flag = false;
private SecureSingleton() {
// 防御反射攻击
if (flag) {
throw new RuntimeException("单例模式禁止反射调用");
}
flag = true;
}
public static SecureSingleton getInstance() {
if (instance == null) {
synchronized (SecureSingleton.class) {
if (instance == null) {
instance = new SecureSingleton();
}
}
}
return instance;
}
}
序列化防御:
public class SerializableSingleton implements Serializable {
private static final long serialVersionUID = 1L;
private static volatile SerializableSingleton instance;
private SerializableSingleton() {}
public static SerializableSingleton getInstance() {
if (instance == null) {
synchronized (SerializableSingleton.class) {
if (instance == null) {
instance = new SerializableSingleton();
}
}
}
return instance;
}
// 反序列化时返回单例实例
protected Object readResolve() {
return getInstance();
}
}
十、总结
单例模式在多线程环境下的实现需要考虑线程安全、资源竞争、性能等多个维度。不同实现方式各有优劣:
- 饿汉式:简单高效,适用于实例创建开销小的场景
- 懒汉式(同步方法):线程安全但性能差,不推荐
- 双重检查锁定(DCL):性能与延迟加载的平衡,需注意volatile的使用
- 静态内部类:推荐方案,兼顾延迟加载、线程安全和性能
- 枚举:最安全的方案,防止反射和序列化攻击
- ThreadLocal:适用于线程隔离场景
在实际开发中,应根据具体需求选择合适的实现方式,并注意处理资源竞争、状态一致性等潜在问题。同时,考虑使用枚举或静态内部类等更安全、更简洁的实现方式,避免过度设计带来的复杂性。
