线程安全问题
下面用一个简单的银行账户转账例子,结合代码和图示说明多线程安全问题的四个必要因素:
一、临界资源共享(共享账户)
public class BankAccount {private int balance = 1000; // 共享资源:账户余额public void transfer(int amount) {balance = balance - amount; // 写操作:转出金额}
}
- 问题:多个线程同时调用
transfer()
修改balance
,导致数据冲突。
二、线程切换(操作交错)
假设两个线程同时执行transfer(500)
:
// 线程A执行(时间线1)
1. 读取balance = 1000
2. 计算1000 - 500 = 500// 此时线程A被CPU调度挂起...// 线程B执行(时间线2)
3. 读取balance = 1000(因为线程A未写入新值)
4. 计算1000 - 500 = 500
5. 写入balance = 500// 线程A恢复执行
6. 写入balance = 500(覆盖线程B的结果)
- 结果:两次转出500元后,余额应为0,但实际仍为500元。
三、原子性操作缺失(非原子的写)
balance = balance - amount
分解为三个步骤:
- 读取
balance
的值 - 计算
balance - amount
- 将结果写回
balance
非原子性导致步骤间可能被其他线程打断。
四、内存可见性问题(缓存不一致)
public class BankAccount {private int balance = 1000; // 未声明volatile,存在可见性问题public void transfer(int amount) {balance = balance - amount;}
}
- 场景:
- 线程A将
balance
缓存到CPU寄存器。 - 线程B修改主内存中
balance
的值。 - 线程A继续使用寄存器中的旧值,导致计算错误。
- 线程A将
解决方案示例(破坏必要因素)
public class BankAccount {private volatile int balance = 1000; // 用volatile保证可见性public synchronized void transfer(int amount) { // 用synchronized保证原子性balance = balance - amount;}
}
- 原理:
synchronized
保证同一时间只有一个线程执行transfer()
(破坏竞争条件)。volatile
保证每次读取都从主内存获取最新值(解决可见性问题)。
总结
必要因素 | 示例中的体现 | 解决方案 |
---|---|---|
临界资源共享 | 多个线程共享balance 变量 | 避免共享(如使用ThreadLocal) |
线程切换 | 线程执行过程中被CPU调度 | 用锁(synchronized)强制串行化 |
原子性操作缺失 | balance = balance - amount 非原子 | 用原子类(AtomicInteger)或锁 |
内存可见性问题 | 线程间缓存数据不同步 | 用volatile或锁保证内存同步 |