Java并发BUG案例

wen java案例 1

本文目录导读:

Java并发BUG案例

  1. 案例1:可见性问题(Visibility)
  2. 案例2:非原子操作(Race Condition)
  3. 案例3:死锁(Deadlock)
  4. 案例4:线程安全性问题(ArrayList)
  5. 案例5:单例模式双重检查锁问题

下面我为你提供几个经典的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++由读取、修改、写入三个步骤组成,多个线程同时执行时会产生数据竞争
  • 解决方案:使用AtomicIntegersynchronizedLock保证原子性

修复方法

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不是线程安全的,多个线程同时修改内部数组结构时会导致数据丢失或位置错乱
  • 解决方案:使用CopyOnWriteArrayListVectorCollections.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并发编程中的典型问题,在实际开发中,建议遵循以下原则:

  1. 使用线程安全的数据结构和工具类
  2. 注意可见性、原子性和有序性
  3. 设计无锁方案或合理的锁策略
  4. 使用并发工具类如CountDownLatchSemaphore

抱歉,评论功能暂时关闭!