在 Java 并发编程中,线程安全问题主要发生在多个线程同时访问共享资源且未正确同步时。以下是常见的线程安全问题及其原理和示例:
1. 竞态条件(Race Condition)
- 问题:多个线程对同一共享数据执行"读-改-写"操作时,最终结果取决于线程执行的时序
- 经典场景:计数器自增(
i++
)public class Counter { private int count = 0; public void increment() { count++; // 非原子操作:实际是 read -> modify -> write } }
- 两个线程同时执行
increment()
可能导致结果小于预期(如预期 20000 实际 19876)
- 两个线程同时执行
2. 可见性问题(Visibility)
- 问题:一个线程修改共享变量后,其他线程无法立即看到最新值
- 原因:CPU 缓存、编译器指令重排序
- 示例:
public class VisibilityIssue { private boolean flag = true; // 未用 volatile 修饰 public void writer() { flag = false; // 线程A修改 } public void reader() { while (flag) { // 线程B可能永远看不到修改 // 死循环... } } }
3. 死锁(Deadlock)
- 问题:两个或多个线程互相持有对方需要的锁,导致永久阻塞
- 四个必要条件:互斥、持有等待、不可剥夺、循环等待
- 示例:
// 线程1 synchronized(lockA) { synchronized(lockB) { ... } // 等待lockB } // 线程2 synchronized(lockB) { synchronized(lockA) { ... } // 等待lockA }
4. 脏读(Dirty Read)
- 问题:读取到未提交的中间状态数据
- 场景:非原子性操作过程中读取数据
public class Account { private int balance = 1000; // 非原子转账操作 public void transfer(int amount) { balance += amount; // 步骤1 // 此时其他线程读取可能得到错误余额 balance -= fee; // 步骤2 } }
5. 活锁(Livelock)
- 问题:线程不断重试失败操作,无法继续执行
- 示例:两个线程互相"礼让"
// 线程1 while (!tryLock(lockA)) { release(lockB); // 总是释放资源重试 } // 线程2 while (!tryLock(lockB)) { release(lockA); // 对称释放 }
6. 资源饥饿(Starvation)
- 问题:某些线程长期无法获取所需资源
- 常见原因:
- 线程优先级设置不合理
- 锁被某些线程长期占用
- 线程池任务分配不均
7. 发布逸出(Escape)
- 问题:在对象构造完成前暴露引用
- 危险示例:
public class ThisEscape { private int value; public ThisEscape() { new Thread(() -> { System.out.println(this.value); // 可能读到未初始化的0 }).start(); value = 42; // 初始化 } }
8. 集合类线程不安全
- 典型问题:
ArrayList
并发修改导致ConcurrentModificationException
HashMap
并发 put 导致链表成环(JDK7)
// 错误示例 List<String> list = new ArrayList<>(); // 多线程同时调用 list.add("item") 会破坏内部结构
9. 原子性破坏
- 问题:复合操作被中途打断
- 经典案例:检查后执行(Check-Then-Act)
if (!map.containsKey(key)) { // 检查 map.put(key, value); // 执行(可能被其他线程插入) }
🔐 解决方案与最佳实践
-
使用锁机制:
// synchronized 同步 public synchronized void safeIncrement() { count++; } // ReentrantLock private Lock lock = new ReentrantLock(); public void safeMethod() { lock.lock(); try { /* 操作 */ } finally { lock.unlock(); } }
-
原子变量:
private AtomicInteger atomicCount = new AtomicInteger(0); atomicCount.incrementAndGet(); // 线程安全的自增
-
线程安全容器:
Map<String, String> safeMap = new ConcurrentHashMap<>(); List<String> safeList = new CopyOnWriteArrayList<>();
-
volatile 关键字:
private volatile boolean flag; // 保证可见性
-
避免共享:
// 使用 ThreadLocal private static ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
-
不可变对象:
public final class ImmutablePoint { private final int x; private final int y; // 构造后状态不可变 }
-
高级并发工具:
// 使用 CountDownLatch 协调线程 CountDownLatch latch = new CountDownLatch(2); // 线程完成任务后调用 latch.countDown() latch.await(); // 等待所有完成
📊 线程安全问题分类总结
问题类型 | 典型表现 | 解决方案 |
---|---|---|
竞态条件 | 数据不一致(如计数错误) | 同步锁、原子变量 |
可见性问题 | 读取到过时数据 | volatile、final、锁 |
死锁/活锁 | 线程永久阻塞 | 锁顺序、超时机制 |
集合线程不安全 | ConcurrentModificationException | ConcurrentHashMap 等 |
原子性破坏 | 中间状态暴露 | 同步代码块、原子操作 |
重要原则:优先使用
java.util.concurrent
包中的并发工具(如线程池、并发集合、原子类),而非手动实现同步逻辑。