🔒 线程安全问题
深入理解多线程环境下的安全隐患,掌握线程安全问题的识别和解决方法
学习目标
- 深入理解线程安全问题的本质
- 识别常见的线程安全问题
- 掌握线程安全问题的分析方法
- 学会设计线程安全的代码
- 了解Java内存模型对线程安全的影响
什么是线程安全问题
线程安全问题是指在多线程环境下,由于多个线程同时访问共享资源而导致的数据不一致、程序行为不可预期的问题。这些问题往往难以重现和调试,是并发编程中最具挑战性的部分。
线程安全问题的根本原因在于:多个线程对共享数据的并发访问缺乏适当的同步机制,导致数据竞争和不一致状态。
线程安全问题的特征
竞态条件(Race Condition)
竞态条件是指程序的正确性依赖于多个线程的相对执行时序。当多个线程同时访问和修改共享数据时,最终结果取决于线程调度的时序,这种不确定性就是竞态条件。
经典案例:计数器问题
public class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // 这不是原子操作!
}
public int getCount() {
return count;
}
}
// 测试代码
public class RaceConditionDemo {
public static void main(String[] args) throws InterruptedException {
UnsafeCounter counter = new UnsafeCounter();
int threadCount = 10;
int incrementsPerThread = 1000;
Thread[] threads = new Thread[threadCount];
for (int i = 0; i < threadCount; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < incrementsPerThread; j++) {
counter.increment();
}
});
}
// 启动所有线程
for (Thread thread : threads) {
thread.start();
}
// 等待所有线程完成
for (Thread thread : threads) {
thread.join();
}
System.out.println("期望结果: " + (threadCount * incrementsPerThread));
System.out.println("实际结果: " + counter.getCount());
// 实际结果通常小于期望结果
}
}
count++
操作实际上包含三个步骤:
- 读取count的当前值
- 将值加1
- 将新值写回count
当多个线程同时执行这三个步骤时,可能会相互干扰,导致数据丢失。
银行转账的竞态条件
public class BankAccount {
private double balance;
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
public void withdraw(double amount) {
if (balance >= amount) { // 检查余额
// 模拟处理时间
try {
Thread.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
balance -= amount; // 扣除金额
System.out.println("取款 " + amount + ",余额: " + balance);
} else {
System.out.println("余额不足,无法取款 " + amount);
}
}
public double getBalance() {
return balance;
}
}
// 测试并发取款
public class BankTransferDemo {
public static void main(String[] args) throws InterruptedException {
BankAccount account = new BankAccount(1000.0);
// 创建多个线程同时取款
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++) {
threads[i] = new Thread(() -> {
account.withdraw(300.0);
});
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("最终余额: " + account.getBalance());
// 可能出现负数余额!
}
}
原子性问题
原子性是指一个操作要么完全执行,要么完全不执行,不能被其他线程中断。在多线程环境下,非原子操作可能被其他线程干扰,导致数据不一致。
什么操作不是原子的?
- i++ 和 ++i
- i += 5
- 复合赋值操作
- if (condition) { action(); }
- 懒加载模式
- 单例模式的双重检查
- long类型的读写
- double类型的读写
- 在32位JVM上可能不是原子的
原子性问题示例
public class AtomicityProblem {
private long value = 0;
public void setValue(long newValue) {
this.value = newValue; // 在32位JVM上可能不是原子的
}
public long getValue() {
return value; // 在32位JVM上可能不是原子的
}
}
// 可能读取到部分更新的值
public class PartialUpdateDemo {
public static void main(String[] args) {
AtomicityProblem problem = new AtomicityProblem();
// 写线程
Thread writer = new Thread(() -> {
for (int i = 0; i < 1000000; i++) {
problem.setValue(0x0123456789ABCDEFL);
problem.setValue(0xFEDCBA9876543210L);
}
});
// 读线程
Thread reader = new Thread(() -> {
for (int i = 0; i < 1000000; i++) {
long value = problem.getValue();
if (value != 0x0123456789ABCDEFL &&
value != 0xFEDCBA9876543210L &&
value != 0) {
System.out.println("检测到部分更新: " +
Long.toHexString(value));
}
}
});
writer.start();
reader.start();
}
}
可见性问题
可见性问题是指一个线程对共享变量的修改,其他线程不能立即看到。这是由于现代计算机的内存模型和CPU缓存机制导致的。
可见性问题的原因
可见性问题示例
public class VisibilityProblem {
private boolean flag = false;
private int counter = 0;
public void writer() {
counter = 42;
flag = true; // 设置标志位
}
public void reader() {
if (flag) { // 可能永远看不到flag的变化
System.out.println("Counter: " + counter);
// counter的值可能不是42!
}
}
}
// 演示可见性问题
public class VisibilityDemo {
public static void main(String[] args) throws InterruptedException {
VisibilityProblem problem = new VisibilityProblem();
// 读线程
Thread reader = new Thread(() -> {
while (true) {
problem.reader();
}
});
reader.start();
Thread.sleep(1000);
// 写线程
Thread writer = new Thread(() -> {
problem.writer();
System.out.println("Writer finished");
});
writer.start();
writer.join();
// 读线程可能永远不会输出"Counter: 42"
Thread.sleep(2000);
System.exit(0);
}
}
使用volatile
关键字可以解决可见性问题:
private volatile boolean flag = false;
private volatile int counter = 0;
volatile
确保变量的读写直接在主内存中进行,保证了可见性。
有序性问题
有序性问题是指程序执行的顺序可能与代码编写的顺序不一致。这是由于编译器和处理器为了优化性能而进行的指令重排序导致的。
指令重排序的类型
有序性问题示例
public class ReorderingProblem {
private int a = 0;
private boolean flag = false;
public void writer() {
a = 1; // 操作1
flag = true; // 操作2
// 由于重排序,操作2可能在操作1之前执行
}
public void reader() {
if (flag) { // 操作3
int i = a * a; // 操作4
// 如果发生重排序,这里的a可能还是0
System.out.println("a * a = " + i);
}
}
}
// 双重检查锁定的有序性问题
public class DoubleCheckedLocking {
private volatile static DoubleCheckedLocking instance;
public static DoubleCheckedLocking getInstance() {
if (instance == null) { // 第一次检查
synchronized (DoubleCheckedLocking.class) {
if (instance == null) { // 第二次检查
instance = new DoubleCheckedLocking();
// 这里可能发生重排序!
// 1. 分配内存空间
// 2. 调用构造函数初始化对象
// 3. 将instance指向分配的内存地址
// 如果2和3重排序,其他线程可能看到未初始化的对象
}
}
}
return instance;
}
}
Java内存模型通过happens-before规则来保证有序性:
- 程序顺序规则:单线程内,按照程序代码顺序执行
- 监视器锁规则:unlock操作happens-before后续的lock操作
- volatile变量规则:volatile写happens-before后续的volatile读
- 传递性:如果A happens-before B,B happens-before C,则A happens-before C
线程安全的设计原则
设计线程安全的代码需要遵循一些基本原则,这些原则可以帮助我们避免常见的线程安全问题。
核心设计原则
创建不可变的对象,一旦创建就不能修改其状态。
public final class ImmutablePerson {
private final String name;
private final int age;
public ImmutablePerson(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
}
将数据封闭在单个线程中,避免共享。
// ThreadLocal实现线程封闭
public class ThreadLocalExample {
private static ThreadLocal formatter =
ThreadLocal.withInitial(() ->
new SimpleDateFormat("yyyy-MM-dd"));
public String formatDate(Date date) {
return formatter.get().format(date);
}
}
使用适当的同步机制保护共享数据。
public class SafeCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
最佳实践
- 最小化共享:尽量减少线程间的共享数据
- 使用并发工具类:优先使用java.util.concurrent包中的工具
- 避免过度同步:同步的粒度要适当,避免性能问题
- 文档化线程安全性:明确标注类的线程安全级别
- 测试并发代码:使用压力测试和并发测试工具