本文目录导读:

下面我为你提供几个经典的Java并发BUG案例,每个案例都附有代码示例和详细分析,帮助你理解并发问题的本质。
案例1:可见性问题(Visibility)
代码示例
public class VisibilityBug {
private static boolean running = true;
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
while (running) {
// 空循环等待标志位改变
}
System.out.println("Worker线程结束");
});
worker.start();
Thread.sleep(1000);
running = false; // 主线程修改标志位
System.out.println("主线程已修改标志位");
worker.join();
}
}
BUG分析
- 问题:主线程修改
running = false后,Worker线程可能永远无法退出while循环 - 原因:Worker线程可能将
running的值缓存在CPU缓存中,没有及时看到主线程的修改 - 解决方案:使用
volatile关键字或synchronized保证可见性
修复方法
private static volatile boolean running = true; // 添加volatile
案例2:非原子操作(Race Condition)
代码示例
public class CounterRaceCondition {
private int count = 0;
public void increment() {
count++; // 非原子操作:读-改-写
}
public static void main(String[] args) throws InterruptedException {
CounterRaceCondition counter = new CounterRaceCondition();
// 创建100个线程,每个线程递增1000次
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Thread t = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
threads.add(t);
t.start();
}
for (Thread t : threads) {
t.join();
}
System.out.println("最终计数: " + counter.count); // 期望100000
}
}
BUG分析
- 问题:程序运行多次会得到不同的结果,通常小于100000
- 原因:
count++由读取、修改、写入三个步骤组成,多个线程同时执行时会产生数据竞争 - 解决方案:使用
AtomicInteger、synchronized或Lock保证原子性
修复方法
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
案例3:死锁(Deadlock)
代码示例
public class DeadlockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
// 线程1:先获取lock1,再获取lock2
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread1: 获得了lock1");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (lock2) {
System.out.println("Thread1: 获得了lock2");
}
}
});
// 线程2:先获取lock2,再获取lock1
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread2: 获得了lock2");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (lock1) {
System.out.println("Thread2: 获得了lock1");
}
}
});
thread1.start();
thread2.start();
}
}
BUG分析
- 问题:程序运行后可能永远卡住,两个线程互相持有对方需要的锁
- 原因:锁的竞争顺序不一致,形成循环等待
- 解决方案:固定锁的获取顺序,或使用
tryLock搭配超时
修复方法
// 方法1:固定锁顺序
// 方法2:使用ReentrantLock的tryLock
ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();
if (lock1.tryLock()) {
try {
if (lock2.tryLock()) {
try { /* 业务逻辑 */ } finally { lock2.unlock(); }
}
} finally { lock1.unlock(); }
}
案例4:线程安全性问题(ArrayList)
代码示例
public class ArrayListThreadSafety {
private static ArrayList<Integer> list = new ArrayList<>();
public static void main(String[] args) throws InterruptedException {
List<Integer> task1 = new ArrayList<>();
List<Integer> task2 = new ArrayList<>();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
list.add(i);
task1.add(i);
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
list.add(i);
task2.add(i);
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("list大小: " + list.size()); // 期望20000
System.out.println("task1大小: " + task1.size() + ", task2大小: " + task2.size());
System.out.println("两线程成功添加总数: " + (task1.size() + task2.size()));
}
}
BUG分析
- 问题:list.size()可能小于20000,甚至出现数组越界异常
- 原因:
ArrayList不是线程安全的,多个线程同时修改内部数组结构时会导致数据丢失或位置错乱 - 解决方案:使用
CopyOnWriteArrayList、Vector或Collections.synchronizedList()
修复方法
private static List<Integer> list =
Collections.synchronizedList(new ArrayList<>());
// 或
private static CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
案例5:单例模式双重检查锁问题
代码示例
public class SingletonDCL {
private static SingletonDCL instance;
public static SingletonDCL getInstance() {
if (instance == null) { // 第一次检查
synchronized (SingletonDCL.class) {
if (instance == null) { // 第二次检查
instance = new SingletonDCL(); // 非原子操作
}
}
}
return instance;
}
private SingletonDCL() {}
}
BUG分析
- 问题:可能返回一个尚未完全初始化的实例
- 原因:
instance = new SingletonDCL()包含三步(分配内存、初始化对象、赋值),JVM可能重排序导致先赋值再初始化 - 解决方案:使用
volatile关键字或静态内部类方式
修复方法
private static volatile SingletonDCL instance; // 添加volatile
这些案例展示了Java并发编程中的典型问题,在实际开发中,建议遵循以下原则:
- 使用线程安全的数据结构和工具类
- 注意可见性、原子性和有序性
- 设计无锁方案或合理的锁策略
- 使用并发工具类如
CountDownLatch、Semaphore等