Java并发编程--24-死锁排查与性能调优:线上并发问题诊断指南(死锁,CPU飙升,内存溢出)

死锁排查与性能调优:线上并发问题诊断指南

作者 :Weisian
发布时间:2026年3月

直击痛点

"线上系统突然卡死,所有请求都超时,你慌了;CPU飙到200%,服务器报警,你只知道重启;面试官问'怎么排查死锁',你说'用jstack',然后呢?怎么定位哪个线程死锁?怎么找代码位置?'"

在Java并发编程的世界里,死锁和性能问题是最棘手的线上故障

  • 死锁:两个线程互相等待对方释放资源,双双"僵死",系统瘫痪;
  • CPU飙升:某个线程死循环或频繁自旋,把CPU跑满,其他任务排队等死;
  • 内存溢出:线程数过多或对象未释放,OOM导致服务崩溃;
  • 面试高频问:"死锁的四个必要条件是什么?""如何用jstack定位死锁?""CPU 100%怎么排查?""Arthas的thread命令怎么用?"------答不上来=错失高薪offer。

本文将从实战场景 切入,结合工具链排查步骤案例分析 ,彻底讲透死锁排查与性能调优的全流程:

✅ 死锁原理:四个必要条件 + 转账案例复现;

✅ 死锁预防:固定顺序获取锁、定时尝试锁;

✅ 死锁检测:jps、jstack、jconsole、jvisualvm实战;

✅ Arthas在线诊断:thread命令、watch命令、热更新;

✅ CPU飙升排查:top → ps → jstack 三板斧;

✅ 内存溢出排查:jmap、jstat、MAT分析;

✅ 性能调优三板斧:减少锁粒度、缩短锁持有时间、无锁数据结构;

✅ 实战演练:模拟死锁并使用工具完整排查;

✅ 面试高频真题标准答案(直接背)。

📌 核心一句话

死锁是线程互相僵持、谁都不放手 的僵局,必须同时满足4个条件;排查死锁用jstack/Arthas,排查CPU飙升用top+jstack+火焰图;预防死锁核心是打破任意一个必要条件 ,性能调优核心是少加锁、快放锁、用无锁
📌 排查金句先记牢

  • 死锁四要素:互斥、请求保持、不可剥夺、循环等待,缺一不可;

  • 排查死锁三步走:jps找进程 → jstack查线程栈 → 搜索deadlock关键字;

  • CPU 100%排查四步:top定进程 → top -H定线程 → 转16进制 → jstack查代码;

  • Arthas一键命令:thread查死锁,thread -n 3查CPU最高线程,profiler生成火焰图;

  • 打破循环等待是最实用的死锁预防方案:固定锁的获取顺序

  • 性能调优核心:锁的粒度越小越好,持有锁的时间越短越好。
    📌 生活类比先记牢

  • 死锁:两个人过独木桥,你等我让,我等你让,谁也过不去。

  • CPU飙升:一个人发疯一样原地转圈(死循环),把路占满,别人都走不动。

  • 线程阻塞:你在门口等快递,快递员一直不来,你就一直等着。

  • 锁粒度:整个图书馆一次只能进一个人(粗粒度)vs 每个书架可以单独借阅(细粒度)。


一、死锁原理:从理论到复现

1.1 死锁的四个必要条件

生活化类比

两个人(线程A、线程B)去餐厅吃饭,都需要筷子+碗才能吃饭:

  • 线程A抢到了筷子 (持有锁1),等着要(请求锁2);
  • 线程B抢到了 (持有锁2),等着要筷子(请求锁1);
  • 双方都不肯放手自己的资源,也拿不到对方的资源,永远僵持------这就是死锁。

四个必要条件(缺一不可):

  1. 互斥条件:资源不能被共享,一次只能一个线程使用;
  2. 请求与保持:线程已经持有了一个资源,又在等待另一个资源,且不释放已持有的;
  3. 不可剥夺条件:已经获得的资源,在未使用完之前不能被强行剥夺;
  4. 循环等待条件:线程A等B,B等C,C等A,形成环路。

1.2 死锁代码复现

java 复制代码
/**
 * 经典死锁场景:两个线程互相持有对方需要的锁
 */
public class DeadlockDemo {
    // 两个锁对象
    private static final Object LOCK_A = new Object();
    private static final Object LOCK_B = new Object();
    
    public static void main(String[] args) {
        // 线程1:先拿LOCK_A,再拿LOCK_B
        Thread thread1 = new Thread(() -> {
            synchronized (LOCK_A) {
                System.out.println("Thread1: 获得锁A");
                try {
                    Thread.sleep(100); // 模拟业务处理
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread1: 等待锁B");
                synchronized (LOCK_B) {
                    System.out.println("Thread1: 获得锁B");
                }
            }
        }, "Thread-1");
        
        // 线程2:先拿LOCK_B,再拿LOCK_A
        Thread thread2 = new Thread(() -> {
            synchronized (LOCK_B) {
                System.out.println("Thread2: 获得锁B");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread2: 等待锁A");
                synchronized (LOCK_A) {
                    System.out.println("Thread2: 获得锁A");
                }
            }
        }, "Thread-2");
        
        thread1.start();
        thread2.start();
        
        // 主线程等待一段时间后退出
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("程序结束,但实际线程还在死锁中...");
    }
}

运行结果

复制代码
Thread1: 获得锁A
Thread2: 获得锁B
Thread1: 等待锁B
Thread2: 等待锁A
程序结束,但实际线程还在死锁中...

1.3 实战案例:转账场景死锁

java 复制代码
/**
 * 转账场景死锁:两个账户同时互相转账
 */
public class TransferDeadlockDemo {
    
    static class Account {
        private final String name;
        private int balance;
        
        public Account(String name, int balance) {
            this.name = name;
            this.balance = balance;
        }
        
        public void transfer(Account target, int amount) {
            // 先锁自己,再锁对方(死锁风险)
            synchronized (this) {
                System.out.println(Thread.currentThread().getName() + " 锁定账户: " + this.name);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (target) {
                    System.out.println(Thread.currentThread().getName() + " 锁定账户: " + target.name);
                    if (this.balance >= amount) {
                        this.balance -= amount;
                        target.balance += amount;
                        System.out.println(Thread.currentThread().getName() + " 转账成功: " + amount);
                    } else {
                        System.out.println(Thread.currentThread().getName() + " 余额不足");
                    }
                }
            }
        }
        
        @Override
        public String toString() {
            return "Account{name='" + name + "', balance=" + balance + "}";
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        Account accountA = new Account("A", 1000);
        Account accountB = new Account("B", 1000);
        
        // 线程1:A转给B 100元
        Thread t1 = new Thread(() -> accountA.transfer(accountB, 100), "转账线程-A→B");
        // 线程2:B转给A 100元
        Thread t2 = new Thread(() -> accountB.transfer(accountA, 100), "转账线程-B→A");
        
        t1.start();
        t2.start();
        
        t1.join();
        t2.join();
        
        System.out.println("最终结果: " + accountA + ", " + accountB);
    }
}

二、死锁预防与避免策略

2.1 策略一:固定顺序获取锁(最常用)

核心思想:所有线程按照相同的顺序获取锁,破坏循环等待条件。

java 复制代码
/**
 * 转账场景:按账户ID排序,避免死锁
 */
public class SafeTransferDemo {
    
    static class Account {
        private final int id; // 唯一标识
        private final String name;
        private int balance;
        
        public Account(int id, String name, int balance) {
            this.id = id;
            this.name = name;
            this.balance = balance;
        }
        
        /**
         * 安全转账:按ID排序获取锁
         */
        public void transfer(Account target, int amount) {
            Account first = this.id < target.id ? this : target;
            Account second = this.id < target.id ? target : this;
            
            // 先锁ID小的,再锁ID大的
            synchronized (first) {
                System.out.println(Thread.currentThread().getName() + " 锁定账户: " + first.name);
                synchronized (second) {
                    System.out.println(Thread.currentThread().getName() + " 锁定账户: " + second.name);
                    if (this.balance >= amount) {
                        this.balance -= amount;
                        target.balance += amount;
                        System.out.println(Thread.currentThread().getName() + " 转账成功: " + amount);
                    } else {
                        System.out.println(Thread.currentThread().getName() + " 余额不足");
                    }
                }
            }
        }
        
        @Override
        public String toString() {
            return "Account{name='" + name + "', balance=" + balance + "}";
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        Account accountA = new Account(1, "A", 1000);
        Account accountB = new Account(2, "B", 1000);
        
        Thread t1 = new Thread(() -> accountA.transfer(accountB, 100), "转账线程-A→B");
        Thread t2 = new Thread(() -> accountB.transfer(accountA, 100), "转账线程-B→A");
        
        t1.start();
        t2.start();
        
        t1.join();
        t2.join();
        
        System.out.println("最终结果: " + accountA + ", " + accountB);
    }
}

2.2 策略二:定时尝试锁(tryLock)

核心思想 :使用ReentrantLocktryLock方法,获取不到锁就释放已持有的锁。

java 复制代码
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

/**
 * 使用tryLock避免死锁
 */
public class TryLockDemo {
    
    static class Account {
        private final String name;
        private int balance;
        private final ReentrantLock lock = new ReentrantLock();
        
        public Account(String name, int balance) {
            this.name = name;
            this.balance = balance;
        }
        
        /**
         * 安全转账:使用tryLock,超时则释放锁
         */
        public boolean transfer(Account target, int amount, long timeout) throws InterruptedException {
            long deadline = System.currentTimeMillis() + timeout;
            
            // 尝试获取自己的锁
            while (true) {
                if (this.lock.tryLock(timeout, TimeUnit.MILLISECONDS)) {
                    try {
                        System.out.println(Thread.currentThread().getName() + " 获得自己锁: " + this.name);
                        // 尝试获取对方的锁
                        long remaining = deadline - System.currentTimeMillis();
                        if (target.lock.tryLock(remaining, TimeUnit.MILLISECONDS)) {
                            try {
                                System.out.println(Thread.currentThread().getName() + " 获得对方锁: " + target.name);
                                if (this.balance >= amount) {
                                    this.balance -= amount;
                                    target.balance += amount;
                                    System.out.println(Thread.currentThread().getName() + " 转账成功: " + amount);
                                    return true;
                                } else {
                                    System.out.println(Thread.currentThread().getName() + " 余额不足");
                                    return false;
                                }
                            } finally {
                                target.lock.unlock();
                            }
                        }
                    } finally {
                        this.lock.unlock();
                    }
                }
                // 等待一段时间后重试
                Thread.sleep(10);
            }
        }
        
        @Override
        public String toString() {
            return "Account{name='" + name + "', balance=" + balance + "}";
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        Account accountA = new Account("A", 1000);
        Account accountB = new Account("B", 1000);
        
        Thread t1 = new Thread(() -> {
            try {
                accountA.transfer(accountB, 100, 3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "转账线程-A→B");
        
        Thread t2 = new Thread(() -> {
            try {
                accountB.transfer(accountA, 100, 3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "转账线程-B→A");
        
        t1.start();
        t2.start();
        
        t1.join();
        t2.join();
        
        System.out.println("最终结果: " + accountA + ", " + accountB);
    }
}

2.3 策略三:减少锁粒度

核心思想:用细粒度锁替代粗粒度锁,降低锁竞争。

java 复制代码
/**
 * 细粒度锁示例:分段锁
 */
public class FineGrainedLockDemo {
    
    /**
     * 银行账户:使用分段锁
     */
    static class Bank {
        // 分16个锁段,每个锁保护一部分账户
        private final ReentrantLock[] locks = new ReentrantLock[16];
        private final int[] balances = new int[10000];
        
        public Bank() {
            for (int i = 0; i < locks.length; i++) {
                locks[i] = new ReentrantLock();
            }
        }
        
        /**
         * 获取账户对应的锁(按ID取模)
         */
        private ReentrantLock getLock(int accountId) {
            return locks[accountId % locks.length];
        }
        
        /**
         * 转账:只锁涉及的两个账户对应的锁段
         */
        public void transfer(int fromId, int toId, int amount) {
            // 按ID排序获取锁,避免死锁
            int firstId = Math.min(fromId, toId);
            int secondId = Math.max(fromId, toId);
            
            ReentrantLock firstLock = getLock(firstId);
            ReentrantLock secondLock = getLock(secondId);
            
            firstLock.lock();
            try {
                secondLock.lock();
                try {
                    if (balances[fromId] >= amount) {
                        balances[fromId] -= amount;
                        balances[toId] += amount;
                        System.out.println(Thread.currentThread().getName() + " 转账成功: " + amount);
                    }
                } finally {
                    secondLock.unlock();
                }
            } finally {
                firstLock.unlock();
            }
        }
    }
}

三、死锁检测工具实战

3.1 jps + jstack:命令行排查

步骤1:找到进程ID

bash 复制代码
# 查看所有Java进程
jps -l

# 输出示例:
# 12345 com.example.DeadlockDemo
# 67890 org.jetbrains.jps.cmdline.Launcher

步骤2:使用jstack导出线程栈

bash 复制代码
# 导出线程栈到文件
jstack 12345 > deadlock.log

# 或者直接查看
jstack 12345

步骤3:分析死锁信息

bash 复制代码
# jstack会自动检测死锁
jstack 12345 | grep -A 10 "deadlock"

# 输出示例:
Found one Java-level deadlock:
=============================
"Thread-2":
  waiting to lock monitor 0x00007f8c5c00a500 (object 0x00000000d5f8a7f0, a java.lang.Object),
  which is held by "Thread-1"
"Thread-1":
  waiting to lock monitor 0x00007f8c5c00a780 (object 0x00000000d5f8a7e0, a java.lang.Object),
  which is held by "Thread-2"

Java stack information for the threads listed above:
===================================================
"Thread-2":
        at com.example.DeadlockDemo.lambda$main$1(DeadlockDemo.java:30)
        - waiting to lock <0x00000000d5f8a7f0> (a java.lang.Object)
        - locked <0x00000000d5f8a7e0> (a java.lang.Object)
"Thread-1":
        at com.example.DeadlockDemo.lambda$main$0(DeadlockDemo.java:18)
        - waiting to lock <0x00000000d5f8a7e0> (a java.lang.Object)
        - locked <0x00000000d5f8a7f0> (a java.lang.Object)

关键信息解读

  • Found one Java-level deadlock:发现死锁
  • waiting to lock:等待的锁
  • locked:已经持有的锁
  • 代码行号:直接定位到死锁位置

3.2 jconsole:图形化监控

启动方法

bash 复制代码
# 命令行启动
jconsole

# 或找到进程后连接
jconsole <pid>

死锁检测

  1. 点击"线程"选项卡
  2. 点击"检测死锁"按钮
  3. 查看死锁线程的堆栈信息

3.3 jvisualvm:性能分析工具

启动方法

bash 复制代码
# 命令行启动
jvisualvm

功能

  • 线程监控:实时查看线程状态
  • CPU采样:分析热点方法
  • 内存分析:查看堆内存使用

3.4 Arthas:在线诊断神器

Arthas是阿里巴巴开源的Java诊断工具,无需重启即可在线排查问题。

安装与启动

bash 复制代码
# 下载arthas
curl -O https://arthas.aliyun.com/arthas-boot.jar

# 启动,选择要监控的进程
java -jar arthas-boot.jar

核心命令

3.4.1 thread命令:查看线程状态
bash 复制代码
# 查看所有线程
thread

# 查看死锁线程
thread -b

# 输出示例:
# Found one Java-level deadlock:
# =============================
# "Thread-2":
#   waiting for lock 0x00000000d5f8a7f0
#   which is held by "Thread-1"
# "Thread-1":
#   waiting for lock 0x00000000d5f8a7e0
#   which is held by "Thread-2"

# 查看最繁忙的线程(CPU占用最高)
thread -n 3

# 查看指定线程堆栈
thread <thread-id>
3.4.2 watch命令:监控方法执行
bash 复制代码
# 监控方法调用,打印参数和返回值
watch com.example.TransferDeadlockDemo$Account transfer '{params, returnObj}' -x 2

# 监控方法执行时间超过100ms的调用
watch com.example.TransferDeadlockDemo$Account transfer '{params, throwExp}' '#cost>100' -x 2
3.4.3 dashboard:实时监控面板
bash 复制代码
# 进入实时监控面板
dashboard

显示内容:

  • CPU使用率
  • 内存使用情况
  • 线程状态统计
  • GC情况
3.4.4 热更新代码
bash 复制代码
# 反编译代码
jad com.example.DeadlockDemo > DeadlockDemo.java

# 编辑代码(修复死锁)
vim DeadlockDemo.java

# 重新编译
mc -c <classloader-hash> DeadlockDemo.java

# 热更新
redefine /path/to/DeadlockDemo.class

四、CPU飙升排查

死锁会让程序卡死,CPU 100% 会让服务慢到无法使用,常见原因:死循环、频繁GC、复杂计算、锁竞争严重。

4.1 场景:死循环导致CPU 100%

java 复制代码
/**
 * CPU飙升代码:死循环
 */
public class CpuHighDemo {
    public static void main(String[] args) {
        // 模拟CPU飙升场景
        new Thread(() -> {
            System.out.println("死循环线程启动...");
            while (true) {
                // 空循环,CPU 100%
                // 模拟计算密集型任务
                double d = Math.random() * Math.random();
            }
        }, "Cpu-Burner").start();
        
        // 正常业务线程
        new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(1000);
                    System.out.println("业务线程正常运行...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Biz-Thread").start();
    }
}

4.2 排查步骤

步骤1:top命令找到高CPU进程

bash 复制代码
# 查看系统进程,按CPU排序
top

# 输出示例:
# PID   USER   PR  NI  VIRT   RES   SHR   S  %CPU  %MEM   TIME+   COMMAND
# 12345  root   20  0   2.5g   200m  15m   R  100.0  2.5    1:23.45  java

步骤2:找到进程内的高CPU线程

bash 复制代码
# 查看进程内的线程,按CPU消耗排序
top -Hp 12345

# 输出示例:
# PID   USER   PR  NI  VIRT   RES   SHR   S  %CPU  %MEM   TIME+   COMMAND
# 12346  root   20  0   2.5g   200m  15m   R  100.0  2.5    1:20.12  java
# 12347  root   20  0   2.5g   200m  15m   S   0.0   2.5    0:00.01  java

步骤3:转换线程ID为十六进制

bash 复制代码
# 12346 转十六进制
printf "%x\n" 12346
# 输出:303a

步骤4:jstack导出线程栈

bash 复制代码
# 导出线程栈
jstack 12345 > thread-dump.log

# 查找十六进制线程ID对应的堆栈
grep -A 20 "0x303a" thread-dump.log

输出示例

复制代码
"Cpu-Burner" #12 prio=5 os_prio=0 tid=0x00007f8c5c00a800 nid=0x303a runnable [0x00007f8c5c00b000]
   java.lang.Thread.State: RUNNABLE
        at com.example.CpuHighDemo.lambda$main$0(CpuHighDemo.java:10)
        at com.example.CpuHighDemo$$Lambda$1/0x0000000800c08440.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

关键信息

  • 线程状态:RUNNABLE
  • 代码位置:CpuHighDemo.java:10 → 定位到死循环

4.3 Arthas 快速方案

bash 复制代码
# 查看CPU占用最高的3个线程
thread -n 3

1秒定位问题代码,比手动命令快10倍。

4.4 排查脚本

bash 复制代码
#!/bin/bash
# cpu-high.sh - CPU飙升一键排查脚本

echo "=== 1. 找到高CPU进程 ==="
PID=$(top -b -n 1 | grep java | sort -k9 -r | head -1 | awk '{print $1}')
echo "高CPU进程PID: $PID"

echo "=== 2. 找到高CPU线程 ==="
TID=$(top -Hp $PID -b -n 1 | grep java | sort -k9 -r | head -1 | awk '{print $1}')
echo "高CPU线程TID: $TID"

echo "=== 3. 线程ID转十六进制 ==="
HEX=$(printf "%x" $TID)
echo "十六进制: $HEX"

echo "=== 4. 导出线程栈 ==="
jstack $PID > /tmp/jstack-$PID.log

echo "=== 5. 定位问题代码 ==="
grep -A 20 "nid=0x$HEX" /tmp/jstack-$PID.log

4.5 火焰图(Flame Graph):可视化CPU热点

对于复杂的CPU性能问题,文字堆栈太累。火焰图(Flame Graph)是全球通用的CPU性能分析标准,能直观看到哪段代码占用CPU最高。

  • 横轴:采样次数(CPU占用时间,越宽越耗时)。
  • 纵轴:方法调用栈(哪一个线程导致的)。
  • 越宽的方块:CPU占用越高,就是性能瓶颈。
  • 颜色:不同模块(通常随机分配)。

生成步骤(简版)

  1. 使用 async-profiler 采集数据:

    bash 复制代码
    ./profiler.sh -d 30 -f /tmp/flame.html 12345

    (采集30秒,生成HTML文件)

  2. 浏览器打开 /tmp/flame.html

  3. 看图技巧:找最宽的矩形块,那就是CPU的"热点"。点击可下钻查看调用链。

实战价值:一眼找出死循环、慢方法、频繁GC问题。


五、内存溢出与线程数过多问题

5.1 场景:线程泄漏导致OOM

java 复制代码
/**
 * 线程泄漏:不断创建线程,导致系统资源耗尽
 */
public class ThreadLeakDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(10);
        
        // 错误:不断创建线程池,但从不关闭
        for (int i = 0; i < 100000; i++) {
            // 每次循环都创建新的线程池,旧的未关闭
            ExecutorService leakExecutor = Executors.newFixedThreadPool(10);
            leakExecutor.submit(() -> {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            // 忘记关闭 leakExecutor,导致线程泄漏
            
            if (i % 1000 == 0) {
                System.out.println("已创建 " + i + " 个线程池");
            }
        }
    }
}

5.2 排查步骤

步骤1:查看线程数

bash 复制代码
# 查看进程的线程数
ps -eLf | grep java | wc -l

# 或使用jstack统计
jstack <pid> | grep "Thread" | wc -l

步骤2:使用jstat查看GC情况

bash 复制代码
# 查看GC统计,每1秒输出一次
jstat -gcutil <pid> 1000

# 输出示例:
#   S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
#   0.00  99.90  45.23  78.45  92.34  89.12   123    2.345     5    0.678    3.023

步骤3:使用jmap导出堆内存

bash 复制代码
# 导出堆内存快照
jmap -dump:live,format=b,file=heap.hprof <pid>

步骤4:使用MAT分析堆内存

  1. 用MAT打开heap.hprof
  2. 查看Leak Suspects报告
  3. 找到占用内存最多的对象

5.3 常用监控命令汇总

命令 作用 示例
jps 查看Java进程 jps -l
jstack 查看线程栈 jstack <pid>
jmap 查看堆内存 jmap -heap <pid>
jstat 查看GC统计 jstat -gcutil <pid> 1000
jinfo 查看JVM参数 jinfo -flags <pid>
top 查看系统资源 top -Hp <pid>
arthas 在线诊断 thread -b

六、性能调优三板斧

6.1 第一板斧:减少锁粒度

原则:用细粒度锁替代粗粒度锁

java 复制代码
/**
 * 粗粒度锁(性能差)
 */
public class CoarseLockDemo {
    private final Map<String, Integer> map = new HashMap<>();
    
    // 整个方法加锁,影响所有操作
    public synchronized void put(String key, Integer value) {
        map.put(key, value);
    }
    
    public synchronized Integer get(String key) {
        return map.get(key);
    }
}

/**
 * 细粒度锁(性能好)
 */
public class FineLockDemo {
    private final Map<String, Integer> map = new HashMap<>();
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    
    public void put(String key, Integer value) {
        lock.writeLock().lock();
        try {
            map.put(key, value);
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    public Integer get(String key) {
        lock.readLock().lock();
        try {
            return map.get(key);
        } finally {
            lock.readLock().unlock();
        }
    }
}

6.2 第二板斧:避免长时间持有锁

原则:只在必要时加锁

java 复制代码
/**
 * 锁持有时间过长(性能差)
 */
public class LongLockDemo {
    private final List<String> list = new ArrayList<>();
    
    public void badProcess(String data) {
        synchronized (this) {
            // 模拟耗时操作(不应该在锁内执行)
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            list.add(data);
        }
    }
    
    /**
     * 优化:只在必要的地方加锁
     */
    public void goodProcess(String data) {
        // 耗时操作放在锁外
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // 只在临界区加锁
        synchronized (this) {
            list.add(data);
        }
    }
}

6.3 第三板斧:使用无锁数据结构

原则:能用CAS就不用锁

JDK自带高性能无锁工具,性能远超synchronized

  • 替代HashMap → ConcurrentHashMap
  • 替代ArrayList → CopyOnWriteArrayList
  • 原子计数 → AtomicInteger/LongAdder
java 复制代码
/**
 * 使用AtomicInteger替代synchronized
 */
public class AtomicDemo {
    // 错误:使用synchronized
    private int count1 = 0;
    public synchronized void increment1() {
        count1++;
    }
    
    // 正确:使用AtomicInteger(无锁)
    private AtomicInteger count2 = new AtomicInteger(0);
    public void increment2() {
        count2.incrementAndGet();
    }
}

七、实战演练:完整排查流程

7.1 模拟死锁并排查

java 复制代码
/**
 * 实战:模拟死锁,并完整排查
 */
public class DeadlockExercise {
    private static final Object LOCK1 = new Object();
    private static final Object LOCK2 = new Object();
    
    public static void main(String[] args) {
        System.out.println("进程PID: " + ManagementFactory.getRuntimeMXBean().getName());
        
        // 线程1
        new Thread(() -> {
            synchronized (LOCK1) {
                System.out.println("线程1: 获得锁1");
                sleep(100);
                System.out.println("线程1: 等待锁2");
                synchronized (LOCK2) {
                    System.out.println("线程1: 获得锁2");
                }
            }
        }, "Thread-1").start();
        
        // 线程2
        new Thread(() -> {
            synchronized (LOCK2) {
                System.out.println("线程2: 获得锁2");
                sleep(100);
                System.out.println("线程2: 等待锁1");
                synchronized (LOCK1) {
                    System.out.println("线程2: 获得锁1");
                }
            }
        }, "Thread-2").start();
    }
    
    private static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) {}
    }
}

7.2 完整排查步骤

步骤1:运行程序,获取PID

复制代码
进程PID: 12345@localhost

步骤2:使用jstack排查

bash 复制代码
jstack 12345 | grep -A 20 "deadlock"

输出

复制代码
Found one Java-level deadlock:
=============================
"Thread-2":
  waiting to lock monitor 0x00007f8c5c00a500 (object 0x00000000d5f8a7f0, a java.lang.Object),
  which is held by "Thread-1"
"Thread-1":
  waiting to lock monitor 0x00007f8c5c00a780 (object 0x00000000d5f8a7e0, a java.lang.Object),
  which is held by "Thread-2"

Java stack information for the threads listed above:
===================================================
"Thread-2":
        at com.example.DeadlockExercise.lambda$main$1(DeadlockExercise.java:28)
        - waiting to lock <0x00000000d5f8a7f0> (a java.lang.Object)
        - locked <0x00000000d5f8a7e0> (a java.lang.Object)
"Thread-1":
        at com.example.DeadlockExercise.lambda$main$0(DeadlockExercise.java:16)
        - waiting to lock <0x00000000d5f8a7e0> (a java.lang.Object)
        - locked <0x00000000d5f8a7f0> (a java.lang.Object)

步骤3:分析结果

  • 线程1持有锁1(0x00000000d5f8a7e0),等待锁2(0x00000000d5f8a7f0
  • 线程2持有锁2,等待锁1
  • 死锁位置:DeadlockExercise.java:16DeadlockExercise.java:28

步骤4:修复代码

java 复制代码
// 修复:固定顺序获取锁
// 先锁LOCK1,再锁LOCK2,两个线程都用这个顺序

八、面试高频真题

Q1:死锁的四个必要条件是什么?

答案

  1. 互斥条件:资源不能被共享
  2. 请求与保持:持有资源的同时请求其他资源
  3. 不可剥夺:已获得的资源不能被强行剥夺
  4. 循环等待:线程之间形成循环等待链

Q2:如何用jstack排查死锁?

答案

  1. jps找到进程PID
  2. jstack <pid>导出线程栈
  3. 搜索deadlock关键字,JVM会自动检测并输出死锁信息
  4. 分析waiting to locklocked,定位到具体代码行

Q3:CPU 100%如何排查?

答案

  1. top找到高CPU进程PID
  2. top -Hp <pid>找到高CPU线程TID
  3. printf "%x\n" <tid>转十六进制
  4. jstack <pid> | grep -A 20 <hex>定位到代码行
  5. 分析代码,找到死循环或频繁自旋的位置

Q4:Arthas的thread -b命令有什么作用?

答案

  • thread -b:检测并显示死锁线程
  • 直接输出死锁信息,包括锁ID、持有线程、等待线程、堆栈位置
  • 比jstack更简洁,针对死锁做了优化

Q5:如何预防死锁?

答案

  1. 固定顺序获取锁:所有线程按相同顺序获取锁
  2. 定时尝试锁 :使用tryLock,获取不到就释放已持有的锁
  3. 减少锁粒度:用细粒度锁降低锁竞争
  4. 使用无锁数据结构AtomicIntegerConcurrentHashMap

Q6:读写锁(ReadWriteLock)为什么能提升性能?

答案

  • 读锁共享,写锁互斥
  • 多个读线程可以同时持有锁,提高并发度
  • 适用于读多写少的场景
  • 性能比synchronized提升N倍

Q7:线上OOM如何排查?

答案

  1. jmap -heap <pid>查看堆内存概览
  2. jmap -dump:live,format=b,file=heap.hprof <pid>导出堆内存快照
  3. 用MAT或VisualVM分析,找Leak Suspects
  4. 定位到占用内存最多的对象和代码位置
  5. 分析是否内存泄漏(如ThreadLocal未清理)或内存溢出(如集合无限增长)

总结

1. 核心知识点速记口诀

复制代码
死锁四要素,缺一不可破,
互斥持等环,循环等待祸。
jstack来排查,找BLOCKED线程,
锁ID堆栈行,一眼定乾坤。

CPU飙上天,top先找P,
top -Hp找T,十六进制转,
jstack定位线,代码行现原形。

性能调优三板斧:
粒度要细、持锁要短、无锁优先。

2. 核心工具链总结

工具 用途 常用命令
jps 查看Java进程 jps -l
jstack 线程栈分析 jstack <pid>
jmap 堆内存分析 jmap -heap <pid>
jstat GC监控 jstat -gcutil <pid> 1000
Arthas 在线诊断 thread -b, dashboard
top 系统资源 top -Hp <pid>
MAT 内存分析 分析heap.hprof

3. 核心要点回顾

  1. 死锁 :四要素缺一不可,固定锁顺序是最实用预防方案;
  2. 排查工具:jstack(基础)、jconsole(可视化)、Arthas(生产首选);
  3. CPU排查:进程→线程→16进制→线程栈,Arthas可简化为1条命令;
  4. 性能调优:减少锁粒度、缩短锁时间、优先使用无锁;
  5. 核心原则:死锁重在预防,性能重在少锁。

4. 实战建议

  • 生产环境 :在启动脚本中加入-XX:+HeapDumpOnOutOfMemoryError,OOM时自动dump堆内存
  • 监控告警:配置JVM监控,线程数超限、GC频繁时告警
  • 代码规范 :避免在锁内执行耗时操作,使用tryLock代替synchronized
  • 定期巡检:使用Arthas定期检查线上线程状态

写在最后

从死锁排查到CPU飙升定位,从jstack到Arthas,Java并发问题的诊断是一套完整的工具链 + 方法论。很多开发者遇到线上故障只会重启,殊不知重启只是"治标不治本",真正的问题还在那里。

掌握这些排查工具,不仅能让你在故障发生时快速定位 ,更能让你在代码层面 写出更健壮、更高效的并发程序。记住:好的代码不是没有bug,而是bug出现时能快速定位和修复

如果觉得有帮助,欢迎点赞、收藏、转发!

相关推荐
是娇娇公主~2 小时前
C++迭代器详解
开发语言·c++·stl
qq_148115372 小时前
C++网络编程(Boost.Asio)
开发语言·c++·算法
CSCN新手听安2 小时前
【Qt】Qt概述(三)Qt初识,HelloWorld的创建,对象树
开发语言·qt
-Da-2 小时前
【操作系统学习日记】并发编程中的竞态条件与同步机制:互斥锁与信号量
java·服务器·javascript·数据库·系统架构
2301_804215412 小时前
内存映射文件高级用法
开发语言·c++·算法
luanma1509802 小时前
PHP vs C#:30字秒懂两大语言核心差异
android·开发语言·python·php·laravel
Channing Lewis2 小时前
Python 全局变量调用了一个函数,如何实现每次使用时都运行一次函数获取最新的结果
开发语言·python
CoderCodingNo2 小时前
【GESP】C++八级考试大纲知识点梳理 (5) 代数与平面几何
开发语言·c++
爱喝白开水a2 小时前
春节后普通程序员如何“丝滑”跨行AI:不啃算法,也能拿走AI
java·人工智能·算法·spring·ai·前端框架·大模型