Skip to content

并发问题排查

并发程序的问题,往往难以复现、难以定位、难以修复。

死锁、活锁、竞态条件、内存可见性……每一种都有自己的特点。本文教你用工具和方法,快速定位问题。

并发问题的五种类型

问题类型描述典型症状排查难度
死锁多个线程相互等待对方持有的锁程序无响应容易
活锁线程不断重试但无法前进CPU 高但无进展中等
饥饿某些线程长期无法获得资源部分任务永远不执行中等
竞态条件结果依赖执行时序结果不确定困难
内存可见性线程间数据不一致读到过期数据困难

死锁

死锁演示

java
public class DeadlockDemo {

    private static final Object resourceA = new Object();
    private static final Object resourceB = new Object();

    public static void main(String[] args) {
        // 线程1:先 A 后 B
        Thread t1 = new Thread(() -> {
            synchronized (resourceA) {
                System.out.println("线程1: 已获取资源A");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                synchronized (resourceB) {
                    System.out.println("线程1: 已获取资源B");
                }
            }
        }, "Deadlock-Thread-1");

        // 线程2:先 B 后 A(与线程1相反)
        Thread t2 = new Thread(() -> {
            synchronized (resourceB) {
                System.out.println("线程2: 已获取资源B");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                synchronized (resourceA) {
                    System.out.println("线程2: 已获取资源A");
                }
            }
        }, "Deadlock-Thread-2");

        t1.start();
        t2.start();

        // 等待后检测
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println("程序可能发生死锁,使用 jstack 检测");
    }
}

运行后,程序会卡住。

使用 jstack 检测死锁

bash
# 找到 Java 进程 PID
jps -l

# 导出线程堆栈
jstack <pid> > thread_dump.txt

# 直接查看死锁信息(推荐)
jstack -l <pid>

如果存在死锁,jstack -l 会输出:

Found one Java-level deadlock:
=========================
"Deadlock-Thread-2":
  waiting for monitor lock ...
  java.lang.Thread.State: BLOCKED
"Deadlock-Thread-1":
  waiting for monitor lock ...
  java.lang.Thread.State: BLOCKED

死锁解决方案

方案一:固定加锁顺序

java
// ❌ 危险
void method1() { lock(resourceA); lock(resourceB); }
void method2() { lock(resourceB); lock(resourceA); } // 顺序相反

// ✅ 安全
void method1() { lockA(); lockB(); }
void method2() { lockA(); lockB(); } // 顺序一致

方案二:tryLock

java
public class TryLockDemo {

    private static final ReentrantLock lockA = new ReentrantLock();
    private static final ReentrantLock lockB = new ReentrantLock();

    public static void goodMethod() {
        while (true) {
            if (lockA.tryLock()) {
                try {
                    if (lockB.tryLock()) {
                        try {
                            // 操作
                            return;
                        } finally {
                            lockB.unlock();
                        }
                    }
                } finally {
                    lockA.unlock();
                }
            }
            Thread.sleep(10); // 避免活锁
        }
    }
}

竞态条件

竞态条件演示

java
public class RaceConditionDemo {

    private static int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        int threadCount = 100;

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter++;  // 非原子操作
                }
            }).start();
        }

        Thread.sleep(2000);
        System.out.println("期望值: " + (threadCount * 1000));
        System.out.println("实际值: " + counter);
        System.out.println("丢失更新: " + (threadCount * 1000 - counter));
    }
}

结果不确定,每次运行都不一样。

竞态条件解决

java
public class RaceConditionSolution {

    // 方案1:synchronized
    private static int counter1 = 0;
    private static final Object lock = new Object();

    public static void increment1() {
        synchronized (lock) {
            counter1++;
        }
    }

    // 方案2:AtomicInteger
    private static final AtomicInteger counter2 = new AtomicInteger(0);

    public static void increment2() {
        counter2.incrementAndGet();
    }

    // 方案3:LongAdder(高并发)
    private static final LongAdder counter3 = new LongAdder();

    public static void increment3() {
        counter3.increment();
    }
}

活锁

java
public class LivelockDemo {

    private static final AtomicBoolean busy = new AtomicBoolean(false);

    public static void main(String[] args) {
        IntStream.range(0, 2).forEach(i -> {
            new Thread(() -> {
                while (true) {
                    if (busy.compareAndSet(false, true)) {
                        try {
                            System.out.println("线程" + i + ": 正在工作");
                            Thread.sleep(100);
                        } finally {
                            busy.set(false);
                        }
                    }
                    // 立即重试,没有退让,可能造成活锁
                    Thread.yield();
                }
            }, "Livelock-Thread-" + i).start();
        });
    }
}

活锁的解决方案:加入随机退避时间。

内存可见性问题

java
public class VisibilityIssueDemo {

    // ❌ 普通变量:可能不可见
    private static boolean ready = false;
    private static int number = 0;

    // ✅ volatile 变量:保证可见性
    private static volatile boolean volReady = false;
    private static volatile int volNumber = 0;

    public static void main(String[] args) throws InterruptedException {
        // 内存可见性问题可能表现为:
        // Reader 线程永远看不到 Writer 线程的写入
    }
}

排查工具清单

工具用途命令
jstack线程堆栈 dumpjstack -l <pid>
jconsole图形化监控线程jconsole
jvisualvm可视化线程分析jvisualvm
Arthas阿里诊断工具thread -b 检测死锁
jmcJava Mission Control高级诊断
pidstat线程 CPU 使用Linux 系统

Arthas 常用命令

bash
# 启动 Arthas
java -jar arthas-boot.jar

# 查看所有线程
thread

# 查看线程堆栈
thread <pid>

# 检测死锁
thread -b

# 查看阻塞线程
thread | grep BLOCKED

代码级排查清单

java
public class ConcurrencyChecklist {

    // 1. 共享可变状态检查
    // - 是否有多个线程访问共享变量?
    // - 变量是否被修改?
    // - 是否有适当的同步?

    // 2. 锁顺序检查
    // - 获取多个锁时顺序是否一致?
    // - 是否可能产生死锁?

    // 3. 原子性检查
    // - 复合操作是否原子?
    // - i++ 等操作是否正确同步?

    // 4. 可见性检查
    // - 是否有适当的 happens-before 关系?
    // - volatile/synchronized 是否正确使用?

    // 5. 资源泄漏检查
    // - 锁是否在 finally 中释放?
    // - 线程池是否正确关闭?
    // - ThreadLocal 是否正确清理?
}

问题类型与解决方案对照

问题排查方法解决方案
死锁jstack -l固定锁顺序、tryLock、超时锁
活锁线程 dump随机退避、减少重试
饥饿线程 dump公平锁、调整优先级
竞态条件代码分析synchronized、原子类
可见性jconsole/Arthasvolatile、synchronized

排查最佳实践

  1. 先复现:在测试环境施加高并发压力,提高复现概率
  2. 收集证据:jstack、jconsole、Arthas 一起用
  3. 分析线程状态:BLOCKED、WAITING、TIMED_WAITING 的比例
  4. 检查锁:jstack -l 专门看锁信息
  5. 日志辅助:在关键位置添加线程 ID 和时间戳日志
  6. 代码审查:并发代码 pair review

要点回顾

  • 死锁:固定加锁顺序或使用 tryLock
  • 活锁:加入随机退避
  • 竞态条件:synchronized 或原子类
  • 可见性:volatile 或 synchronized
  • 工具:jstack、jconsole、Arthas 是排查三件宝
  • 预防优于排查:编码阶段就考虑并发安全

相关链接

基于 VitePress 构建