文章目录
-
- 一、核心结论
- 二、线程安全的三个关键要素
-
- [要素1:无状态的 Service 设计](#要素1:无状态的 Service 设计)
- 要素2:方法内使用局部变量
- 要素3:请求级别的数据隔离
- 三、并发执行示意图
- 四、反例:如何写出线程不安全的代码
-
- [❌ 反例1:在 Service 中添加可变实例字段](#❌ 反例1:在 Service 中添加可变实例字段)
- [❌ 反例2:使用共享的缓存但没有正确同步](#❌ 反例2:使用共享的缓存但没有正确同步)
- [❌ 反例3:在多个请求间共享可变对象](#❌ 反例3:在多个请求间共享可变对象)
- [❌ 反例4:延迟初始化没有正确同步](#❌ 反例4:延迟初始化没有正确同步)
- 五、线程安全的最佳实践总结
- 六、线程安全的关键链路
- 七、代码审查检查清单
-
- [✅ 安全指标](#✅ 安全指标)
- [❌ 危险信号](#❌ 危险信号)
- [八、常见问题 FAQ](#八、常见问题 FAQ)
-
- [Q1: Spring 的 @Service 默认是单例,为什么还能线程安全?](#Q1: Spring 的 @Service 默认是单例,为什么还能线程安全?)
- [Q2: 如果必须使用实例字段怎么办?](#Q2: 如果必须使用实例字段怎么办?)
- [Q3: 如何测试线程安全性?](#Q3: 如何测试线程安全性?)
- [Q4: `ConcurrentHashMap` 一定安全吗?](#Q4:
ConcurrentHashMap一定安全吗?)
- 九、实战示例
- 十、参考资料
一、核心结论
Spring 单例 Service 实现线程安全的一种常见方式是依赖两个关键因素:
- 无状态单例 - Service 没有可变的实例字段
- 请求隔离 - 每个请求有独立的数据对象(如 Context、DTO 等)
注意:这是实现线程安全的一种方式,还有其他方式(如使用同步机制、线程安全容器等),但无状态设计是最推荐的做法。
二、线程安全的三个关键要素
要素1:无状态的 Service 设计
java
@Service
public class UserService {
// ✅ 注入的服务引用 - 不可变,指向的是其他无状态单例
@Resource
private UserRepository userRepository;
@Resource
private EmailService emailService;
// ✅ 配置值 - 启动时注入,之后不可变
@Value("${app.api.timeout}")
private Integer apiTimeout;
// ✅ 没有任何可变的实例字段!
public UserDTO processUser(String userId) {
// 业务逻辑处理
User user = userRepository.findById(userId);
return convertToDTO(user);
}
}
为什么安全?
- 所有字段在应用启动后就固定不变
- 多个线程读取同一个不可变值,不会产生竞态条件
- Service 实例被所有线程共享,但没有任何共享的可变状态
要素2:方法内使用局部变量
java
@Service
public class OrderService {
public OrderResult processOrder(OrderRequest request) {
// ✅ 局部变量 - 每个线程有独立的栈空间
List<OrderItem> items = new ArrayList<>();
// ✅ 局部变量
BigDecimal totalAmount = calculateTotal(request);
// ✅ 局部变量
Order order = buildOrder(request, items, totalAmount);
return new OrderResult(order);
}
}
为什么安全?
- 每个线程调用方法时,JVM 会为该线程分配独立的栈帧
- 局部变量存储在线程私有的栈空间中
- 不同线程的局部变量完全隔离,互不影响
- 即使多个线程同时调用同一个方法,它们操作的是各自栈中的局部变量
要素3:请求级别的数据隔离
java
// 使用线程安全的容器管理请求上下文
public class RequestContextManager {
// ✅ 使用 ConcurrentHashMap 保证线程安全
private static ConcurrentHashMap<String, RequestContext> cache = new ConcurrentHashMap<>();
public static RequestContext getOrCreateContext(String requestId) {
// ✅ computeIfAbsent 是原子操作
return cache.computeIfAbsent(requestId, key -> {
return new RequestContext(key);
});
}
}
// Service 中使用
@Service
public class BusinessService {
public void process(String requestId, String data) {
// ✅ 每个请求获取独立的 Context 实例
RequestContext context = RequestContextManager.getOrCreateContext(requestId);
context.setData(data); // 只影响当前请求的 Context
// 处理业务逻辑...
}
}
为什么安全?
- 每个 requestId 对应一个独立的 Context 实例
ConcurrentHashMap保证并发访问安全- 不同请求操作不同的对象,不存在共享数据竞争
- 即使 Service 是单例,但每个请求的数据对象是独立的
三、并发执行示意图
时间轴 ────────────────────────────────────────────────────────────►
Thread-1 (requestId=A) Thread-2 (requestId=B) Thread-3 (requestId=C)
│ │ │
▼ ▼ ▼
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ 局部变量: │ │ 局部变量: │ │ 局部变量: │
│ items_A │ │ items_B │ │ items_C │
│ totalAmount_A │ │ totalAmount_B │ │ totalAmount_C │
└─────────┬─────────┘ └─────────┬─────────┘ └─────────┬─────────┘
│ │ │
▼ ▼ ▼
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ RequestContext_A │ │ RequestContext_B │ │ RequestContext_C │
│ (独立实例) │ │ (独立实例) │ │ (独立实例) │
└───────────────────┘ └───────────────────┘ └───────────────────┘
│ │ │
└───────────────────────┼───────────────────────┘
│
▼
┌─────────────────────────────┐
│ BusinessService │
│ (单例,无状态,共享) │
│ │
│ - 只读取注入的服务 │
│ - 不修改任何实例字段 │
│ - 操作传入的独立对象 │
└─────────────────────────────┘
四、反例:如何写出线程不安全的代码
❌ 反例1:在 Service 中添加可变实例字段
java
@Service
public class UnsafeUserService {
// ❌ 危险!可变的实例字段
private List<String> processingUsers = new ArrayList<>();
// ❌ 危险!可变的实例字段
private User currentUser;
public void processUser(String userId) {
// ❌ 多线程同时操作同一个 List
this.processingUsers.clear(); // Thread-1 清空
this.processingUsers.add(userId); // Thread-2 同时添加 → 数据错乱!
// ❌ 多线程同时修改同一个引用
this.currentUser = userRepository.findById(userId); // Thread-1 设置 user_A
// Thread-2 设置 user_B
// 后续使用时可能拿到错误的 user
}
}
问题分析:
processingUsers被所有线程共享- Thread-1 正在遍历时,Thread-2 可能正在修改,导致
ConcurrentModificationException currentUser被覆盖,导致数据串请求
正确做法:
java
@Service
public class SafeUserService {
// ✅ 无实例字段,所有数据通过参数传递
public void processUser(String userId) {
// ✅ 使用局部变量
List<String> processingUsers = new ArrayList<>();
processingUsers.add(userId);
// ✅ 使用局部变量
User currentUser = userRepository.findById(userId);
// 处理业务逻辑...
}
}
❌ 反例2:使用共享的缓存但没有正确同步
java
@Service
public class UnsafeCacheService {
// ❌ 使用 HashMap 而不是 ConcurrentHashMap
private Map<String, User> userCache = new HashMap<>();
public User getUser(String userId) {
// ❌ 检查-执行 不是原子操作
if (!userCache.containsKey(userId)) { // Thread-1 检查:不存在
// Thread-2 检查:不存在
User user = userRepository.findById(userId); // 两个线程都去查询数据库
userCache.put(userId, user); // 两个线程都写入
// HashMap 并发写入可能导致死循环或数据丢失
}
return userCache.get(userId);
}
}
问题分析:
HashMap不是线程安全的,并发 put 可能导致链表成环(JDK7)或数据丢失- 应该使用
ConcurrentHashMap或加锁
正确做法:
java
@Service
public class SafeCacheService {
// ✅ 使用线程安全的容器
private ConcurrentHashMap<String, User> userCache = new ConcurrentHashMap<>();
public User getUser(String userId) {
// ✅ computeIfAbsent 是原子操作
return userCache.computeIfAbsent(userId, key -> {
return userRepository.findById(key);
});
}
}
❌ 反例3:在多个请求间共享可变对象
java
@Service
public class UnsafeContextService {
// ❌ 所有请求共享同一个可变对象
private static final RequestContext SHARED_CONTEXT = new RequestContext("shared");
public void process(String requestId, String data) {
// ❌ Thread-1 设置 data="用户A的数据"
SHARED_CONTEXT.setData(data);
// ❌ Thread-2 设置 data="用户B的数据",覆盖了 Thread-1 的值
// Thread-1 读取时可能拿到 "用户B的数据" → 数据串请求!
String actualData = SHARED_CONTEXT.getData();
}
}
问题分析:
- 多个请求共享同一个 Context 对象
- 一个请求的数据会覆盖另一个请求的数据
- 导致"数据串请求"的严重 Bug
正确做法:
java
@Service
public class SafeContextService {
// ✅ 使用线程安全的容器,每个请求独立的 Context
private static ConcurrentHashMap<String, RequestContext> contextCache =
new ConcurrentHashMap<>();
public void process(String requestId, String data) {
// ✅ 每个请求获取独立的 Context
RequestContext context = contextCache.computeIfAbsent(requestId,
key -> new RequestContext(key));
context.setData(data); // 只影响当前请求的 Context
}
}
❌ 反例4:延迟初始化没有正确同步
java
@Service
public class UnsafeLazyInitService {
// ❌ 延迟初始化的实例字段
private ExpensiveResource resource;
public void doSomething() {
// ❌ 双重检查锁定的错误实现(没有 volatile)
if (resource == null) { // Thread-1 检查:null
// Thread-2 检查:null
synchronized (this) {
if (resource == null) {
resource = new ExpensiveResource(); // 可能看到部分初始化的对象
}
}
}
resource.use(); // 可能使用未完全初始化的对象
}
}
问题分析:
- 没有
volatile修饰,可能出现指令重排序 - 其他线程可能看到一个"部分构造"的对象
正确做法:
java
@Service
public class SafeLazyInitService {
// ✅ 使用 volatile 保证可见性
private volatile ExpensiveResource resource;
public void doSomething() {
// ✅ 正确的双重检查锁定
if (resource == null) {
synchronized (this) {
if (resource == null) {
resource = new ExpensiveResource();
}
}
}
resource.use();
}
// ✅ 或者使用更简单的方式:在构造时初始化
private final ExpensiveResource resource = new ExpensiveResource();
}
五、线程安全的最佳实践总结
| 实践 | 说明 | 示例 |
|---|---|---|
| 无状态设计 | Service 不持有可变的实例字段 | 只注入其他 Service 和配置值 |
| 使用局部变量 | 方法内的临时数据用局部变量存储 | List<String> list = new ArrayList<>() |
| 请求隔离 | 每个请求使用独立的 Context/DTO 对象 | 使用 ConcurrentHashMap 管理请求上下文 |
| 线程安全容器 | 使用 ConcurrentHashMap 替代 HashMap |
ConcurrentHashMap<String, Object> |
| 不可变对象 | 配置类、DTO 尽量设计为不可变 | 使用 final 字段和不可变集合 |
| 避免共享可变状态 | 如必须共享,使用适当的同步机制 | synchronized、volatile、原子类等 |
六、线程安全的关键链路
┌─────────────────────────────────────────────────────────────────┐
│ 线程安全的关键链路 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ HTTP Request │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 生成唯一 requestId │ ← 每个请求有唯一标识 │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ RequestContextManager │ │
│ │ (ConcurrentHashMap) │ ← 线程安全的容器 │
│ │ │ │
│ │ requestId → RequestContext │ ← 每个请求独立的 Context │
│ └──────────────┬──────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ BusinessService │ │
│ │ (无状态单例) │ ← 只依赖参数和局部变量 │
│ │ │ │
│ │ process(context, ...) │ ← 操作传入的独立 Context │
│ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
核心原则:遵循无状态 Service + 请求隔离 Context 的设计模式,在多线程并发场景下是安全的。
七、代码审查检查清单
在审查 Spring Service 的线程安全性时,可以按以下清单检查:
✅ 安全指标
- Service 类没有可变的实例字段(除了注入的依赖)
- 方法内使用局部变量存储临时数据
- 每个请求使用独立的 Context/DTO 对象
- 使用线程安全的集合类(
ConcurrentHashMap、CopyOnWriteArrayList等) - 共享资源有适当的同步机制(锁、原子类等)
- 延迟初始化的字段使用
volatile或正确的同步
❌ 危险信号
- Service 有可变的实例字段(如
private List list = new ArrayList()) - 使用非线程安全的集合(
HashMap、ArrayList等)作为实例字段 - 多个请求共享同一个可变对象
- 延迟初始化没有使用
volatile或正确的同步 - 静态可变字段没有同步保护
- 在方法间共享可变状态
八、常见问题 FAQ
Q1: Spring 的 @Service 默认是单例,为什么还能线程安全?
A: 单例本身不是问题,问题在于是否有共享的可变状态。如果 Service 是无状态的(只依赖参数和局部变量),那么单例就是安全的。Spring 单例模式的优势是:
- 减少对象创建开销
- 提高性能
- 只要保证无状态,就是线程安全的
Q2: 如果必须使用实例字段怎么办?
A: 有几种方案:
-
使用 ThreadLocal - 为每个线程提供独立的副本
javaprivate ThreadLocal<List<String>> threadLocalList = ThreadLocal.withInitial(ArrayList::new); -
使用同步机制 -
synchronized、ReentrantLock等javaprivate final Object lock = new Object(); private List<String> sharedList = new ArrayList<>(); public void add(String item) { synchronized (lock) { sharedList.add(item); } } -
使用线程安全的集合 -
ConcurrentHashMap、CopyOnWriteArrayList等javaprivate ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>(); -
重构为无状态设计 - 将状态通过参数传递(推荐)
Q3: 如何测试线程安全性?
A: 可以使用并发测试工具:
-
JMeter - 模拟并发 HTTP 请求
-
JUnit + 多线程测试 - 编写并发单元测试
java@Test public void testConcurrentAccess() throws InterruptedException { ExecutorService executor = Executors.newFixedThreadPool(10); CountDownLatch latch = new CountDownLatch(100); for (int i = 0; i < 100; i++) { executor.submit(() -> { try { service.process(...); } finally { latch.countDown(); } }); } latch.await(); // 验证结果 } -
压力测试工具 - 如 Apache Bench、wrk 等
Q4: ConcurrentHashMap 一定安全吗?
A: ConcurrentHashMap 本身是线程安全的,但需要注意:
-
单个操作是原子的 (如
put、get、remove) -
复合操作需要额外同步 (如
check-then-act模式)java// ❌ 不安全:检查-执行不是原子操作 if (!map.containsKey(key)) { map.put(key, value); } // ✅ 安全:使用 computeIfAbsent map.computeIfAbsent(key, k -> value); -
迭代器是弱一致性的 ,不会抛出
ConcurrentModificationException,但可能看到部分更新
九、实战示例
示例1:用户服务(无状态设计)
java
@Service
public class UserService {
@Resource
private UserRepository userRepository;
@Resource
private EmailService emailService;
// ✅ 无实例字段,完全无状态
public UserDTO getUser(String userId) {
// ✅ 使用局部变量
User user = userRepository.findById(userId);
return convertToDTO(user);
}
public void sendWelcomeEmail(String userId) {
// ✅ 使用局部变量
User user = userRepository.findById(userId);
emailService.send(user.getEmail(), "Welcome");
}
}
示例2:订单服务(请求隔离)
java
@Service
public class OrderService {
// ✅ 使用线程安全的容器管理订单上下文
private static ConcurrentHashMap<String, OrderContext> orderContexts =
new ConcurrentHashMap<>();
public void createOrder(String orderId, OrderRequest request) {
// ✅ 每个订单有独立的上下文
OrderContext context = orderContexts.computeIfAbsent(orderId,
key -> new OrderContext(key));
// ✅ 使用局部变量
List<OrderItem> items = buildItems(request);
BigDecimal total = calculateTotal(items);
context.setItems(items);
context.setTotal(total);
// 处理订单创建...
}
}