线程安全
线程安全指多个线程同时访问同一段代码或同一个对象时,无论线程如何交替执行,程序都能得到正确结果,并保持对象状态一致。
线程安全不是“加了锁就安全”,而是要明确共享状态、并发访问路径和同步策略。
# 1. 什么是线程安全
一个类在多线程环境下仍能保持正确性,可以称为线程安全。
public class Counter {
private int count;
public void increment() {
count++;
}
}
count++ 不是线程安全的。
线程 A 读取 count = 0
线程 B 读取 count = 0
线程 A 写回 1
线程 B 写回 1
执行两次 increment,结果却是 1
# 2. 线程安全三要素
| 问题 | 含义 | 解决方式 |
|---|---|---|
| 原子性 | 操作不能被打断 | 锁、原子类 |
| 可见性 | 一个线程修改,其他线程能看到 | volatile、锁、final、安全发布 |
| 有序性 | 禁止破坏语义的重排序 | volatile、锁、happens-before |
很多并发 bug 是三者叠加的结果。
# 3. 共享状态
线程安全问题的根源是共享可变状态。
共享 + 可变 + 多线程访问
│
▼
需要并发控制
没有共享,就没有竞争:
public int add(int a, int b) {
int result = a + b;
return result;
}
局部变量在线程栈中,每个线程独有,通常天然线程安全。局部变量引用的对象如果逃逸到其他线程,则另当别论。
# 4. 竞态条件
竞态条件指程序结果依赖线程执行时序。
if (stock > 0) {
stock--;
}
可能出现:
stock = 1
线程 A 判断 stock > 0 为 true
线程 B 判断 stock > 0 为 true
线程 A stock-- -> 0
线程 B stock-- -> -1
判断和扣减必须作为一个整体保护。
# 5. 线程安全策略
# 5.1 不共享
使用局部变量、方法参数、线程封闭。
public void handle() {
List<String> values = new ArrayList<>();
}
# 5.2 不可变
不可变对象天然线程安全。
public final class Money {
private final BigDecimal amount;
public Money(BigDecimal amount) {
this.amount = amount;
}
}
# 5.3 加锁
public synchronized void increment() {
count++;
}
锁同时提供互斥和可见性。
# 5.4 原子类
AtomicInteger count = new AtomicInteger();
count.incrementAndGet();
适合单变量原子更新。
# 5.5 并发容器
ConcurrentHashMap<String, User> map = new ConcurrentHashMap<>();
不要用普通集合自己拼装复杂同步,除非边界非常清晰。
# 6. 安全发布
对象构造完成后,其他线程必须以安全方式看到它。
安全发布方式:
- 通过
final字段。 - 通过
volatile引用。 - 通过锁保护。
- 放入线程安全容器。
- 在线程启动前完成初始化。
错误示例:
private User user;
public void init() {
user = new User();
}
如果没有同步,其他线程可能看到未完全初始化的对象状态。
# 7. 常见线程不安全类
| 类 | 问题 |
|---|---|
ArrayList | 并发 add 可能丢数据或越界 |
HashMap | 并发 put 状态不安全 |
SimpleDateFormat | 内部状态可变 |
StringBuilder | 可变且非同步 |
普通 int 计数器 | ++ 非原子 |
替代:
| 场景 | 替代 |
|---|---|
| 并发 List 读多写少 | CopyOnWriteArrayList |
| 并发 Map | ConcurrentHashMap |
| 日期格式化 | DateTimeFormatter |
| 计数 | AtomicLong、LongAdder |
| 字符串局部拼接 | 局部 StringBuilder |
# 8. 判断一个类是否线程安全
检查:
- 是否有共享可变字段。
- 字段是否会在多个线程访问。
- 每个访问路径是否有统一同步策略。
- 复合操作是否被整体保护。
- 对象是否安全发布。
- 是否把内部可变集合直接暴露给外部。
# 9. 线程安全与性能
线程安全通常有成本:
- 加锁会有竞争和阻塞。
- 原子类在高竞争下 CAS 重试。
- CopyOnWrite 写入复制数组。
- 不可变对象可能增加对象创建。
不要凭感觉优化,要通过压测和监控判断瓶颈。
# Tips 快问快答
Q:线程安全的根源问题是什么? A:共享可变状态被多个线程同时访问。
Q:局部变量一定线程安全吗? A:局部变量本身通常安全,但它引用的对象如果逃逸到其他线程就不一定。
Q:count++ 为什么不安全?
A:它包含读、加、写多个步骤,不是原子操作。
Q:锁解决哪些问题? A:同时解决互斥、可见性和一定的有序性问题。
Q:不可变对象为什么线程安全? A:对象状态创建后不再变化,多线程读取不会产生竞争。
Q:什么是安全发布? A:让其他线程看到对象引用时,也能看到对象构造完成后的正确状态。
Q:线程安全类一定性能差吗? A:不一定,要看实现和竞争程度,但同步通常有成本。
Q:如何优先避免线程安全问题? A:减少共享状态,优先使用不可变对象和线程封闭。