线程安全问题
外观
概述[编辑 | 编辑源代码]
线程安全问题(Thread Safety)指当多个线程并发访问共享资源时,程序仍能保持正确的行为和数据一致性。若未采取同步措施,可能导致竞态条件(Race Condition)、内存可见性问题或指令重排序等问题,进而引发不可预知的错误。
线程安全的核心挑战在于:
- 原子性:操作不可被中断(如非原子操作的`i++`)
- 可见性:线程对共享变量的修改对其他线程立即可见
- 有序性:程序执行顺序符合代码逻辑(避免指令重排序)
典型问题场景[编辑 | 编辑源代码]
竞态条件示例[编辑 | 编辑源代码]
以下代码展示了一个经典的线程不安全案例:
public class Counter {
private int count = 0;
public void increment() {
count++; // 非原子操作(读取-修改-写入)
}
public int getCount() {
return count;
}
}
当多个线程同时调用`increment()`时,可能发生以下情况: 1. 线程A读取`count=0` 2. 线程B也读取`count=0` 3. 两者分别执行`count+1`并写回 4. 最终结果可能是`1`而非预期的`2`
内存可见性问题[编辑 | 编辑源代码]
public class VisibilityProblem {
private boolean flag = true;
public void writer() {
flag = false; // 修改可能不会立即对其他线程可见
}
public void reader() {
while (flag) {
// 可能陷入无限循环
}
}
}
解决方案[编辑 | 编辑源代码]
同步机制[编辑 | 编辑源代码]
方法 | 描述 | 适用场景 |
---|---|---|
`synchronized` | 通过互斥锁保证原子性和可见性 | 方法/代码块同步 |
`volatile` | 保证可见性,禁止指令重排序 | 单一变量状态标志 |
`AtomicXXX` | CAS(Compare-And-Swap)原子类 | 计数器等场景 |
synchronized示例[编辑 | 编辑源代码]
public class SafeCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
AtomicInteger示例[编辑 | 编辑源代码]
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
}
线程封闭[编辑 | 编辑源代码]
通过限制数据访问权限避免共享:
- 栈封闭:局部变量线程私有
- ThreadLocal:为每个线程创建独立副本
实际应用案例[编辑 | 编辑源代码]
电商库存扣减[编辑 | 编辑源代码]
解决方案:使用数据库悲观锁或分布式锁保证原子性。
深入原理[编辑 | 编辑源代码]
Happens-Before规则[编辑 | 编辑源代码]
Java内存模型定义的保证可见性的规则,包括:
- 程序顺序规则
- 锁规则(解锁先于后续加锁)
- `volatile`变量规则
指令重排序[编辑 | 编辑源代码]
编译器/处理器可能优化指令顺序,通过`volatile`或`synchronized`禁止重排序:
常见误区[编辑 | 编辑源代码]
- 认为`volatile`能替代`synchronized`(实际仅保证可见性)
- 忽略复合操作的非原子性(如`check-then-act`)
- 过度同步导致性能下降
最佳实践[编辑 | 编辑源代码]
1. 优先使用不可变对象(如`String`) 2. 缩小同步代码块范围 3. 使用线程安全集合(如`ConcurrentHashMap`) 4. 避免在同步块中调用外部方法