深度剖析:Java 并发三大量难题 —— 死锁、活锁、饥饿全解

张开发
2026/4/16 19:31:08 15 分钟阅读

分享文章

深度剖析:Java 并发三大量难题 —— 死锁、活锁、饥饿全解
前言在高并发、分布式的Java应用架构中线程安全是保障系统稳定性的核心要素而死锁、活锁、饥饿是并发编程中最隐蔽、最棘手的三大问题。这类问题一旦在生产环境触发会直接导致服务卡顿、线程阻塞、资源耗尽甚至引发系统雪崩。一、并发线程基础概念回顾1.1 线程与锁的核心关系Java多线程通过共享内存实现数据交互为了保证共享资源的原子性、可见性、有序性必须使用锁机制synchronized、ReentrantLock等进行同步控制。锁的本质是互斥访问同一时间只有一个线程能持有锁其他线程进入阻塞状态等待锁释放。但不合理的锁设计、资源竞争策略会直接引发死锁、活锁、饥饿问题。1.2 三类问题的核心区别问题类型核心特征线程状态资源占用死锁线程互相持有对方需要的锁永久阻塞BLOCKED永久占用资源活锁线程不断释放锁又重新争抢无法执行业务RUNNABLE持续消耗CPU饥饿低优先级线程长期获取不到锁无法执行WAITING资源被高优先级线程独占二、死锁Deadlock并发编程头号杀手2.1 死锁的官方定义与产生条件死锁是指两个或多个线程在执行过程中因互相持有对方需要的资源且都不释放自身持有的资源导致永久阻塞的现象。根据《Java并发编程实战》权威定义死锁必须同时满足四个必要条件缺一不可互斥条件资源同一时间只能被一个线程持有请求与保持条件线程持有已获取的资源同时请求其他资源不可剥夺条件线程持有的资源只能主动释放无法被其他线程抢占循环等待条件多个线程形成资源请求的环形链。2.2 死锁流程图解2.3 死锁实战代码案例基于JDK17、Lombok、Spring工具类编写严格遵循阿里巴巴开发手册package com.jam.demo.deadlock; import lombok.extern.slf4j.Slf4j; import org.springframework.util.ObjectUtils; /** * 死锁示例代码 * author ken */ Slf4j public class DeadLockDemo { //定义两个锁资源 private static final Object LOCK_A new Object(); private static final Object LOCK_B new Object(); /** * 线程1先获取LOCK_A再获取LOCK_B */ private static void methodA() { synchronized (LOCK_A) { log.info(线程{}获取到LOCK_A, Thread.currentThread().getName()); try { //模拟业务执行增大死锁概率 Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error(线程A被中断, e); } synchronized (LOCK_B) { log.info(线程{}获取到LOCK_B, Thread.currentThread().getName()); } } } /** * 线程2先获取LOCK_B再获取LOCK_A */ private static void methodB() { synchronized (LOCK_B) { log.info(线程{}获取到LOCK_B, Thread.currentThread().getName()); try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error(线程B被中断, e); } synchronized (LOCK_A) { log.info(线程{}获取到LOCK_A, Thread.currentThread().getName()); } } } public static void main(String[] args) { //启动线程1 new Thread(DeadLockDemo::methodA, Thread-1).start(); //启动线程2 new Thread(DeadLockDemo::methodB, Thread-2).start(); } }代码说明两个线程分别持有LOCK_A和LOCK_B同时请求对方的锁满足死锁四大条件运行后程序永久阻塞。2.4 数据库死锁案例MySQL 8.0分布式场景中数据库行锁死锁是高频问题示例SQL-- 会话1 START TRANSACTION; UPDATE user SET balance balance - 100 WHERE id 1; -- 暂停执行 UPDATE user SET balance balance 100 WHERE id 2; COMMIT; -- 会话2 START TRANSACTION; UPDATE user SET balance balance 100 WHERE id 2; -- 暂停执行 UPDATE user SET balance balance - 100 WHERE id 1; COMMIT;原理两个事务分别持有id1、id2的行锁互相请求对方资源形成数据库死锁。三、活锁Livelock看似运行实则无效3.1 活锁定义与产生原理活锁是指线程没有阻塞始终处于RUNNABLE状态不断释放锁并重新争抢却无法执行业务逻辑的现象。与死锁不同活锁线程会持续占用CPU资源CPU使用率会飙升但业务完全无法推进。活锁产生核心原因线程之间互相谦让资源没有一个线程能稳定持有锁执行任务。3.2 活锁流程图解3.3 活锁实战代码案例package com.jam.demo.livelock; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.atomic.AtomicBoolean; /** * 活锁示例代码 * author ken */ Slf4j public class LiveLockDemo { //资源状态标记 private static final AtomicBoolean RESOURCE new AtomicBoolean(false); /** * 线程工作逻辑检测资源被占用则主动释放 * param threadName 线程名称 * param target 目标状态 */ private static void work(String threadName, boolean target) { while (true) { //尝试设置资源状态 if (RESOURCE.compareAndSet(!target, target)) { log.info(线程{}获取资源执行业务, threadName); try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error(线程中断, e); } //主动释放资源引发活锁 RESOURCE.set(!target); log.info(线程{}主动释放资源, threadName); } else { log.info(线程{}等待资源, threadName); } } } public static void main(String[] args) { new Thread(() - work(Thread-1, true), Thread-1).start(); new Thread(() - work(Thread-2, false), Thread-2).start(); } }代码说明两个线程不断获取并主动释放资源始终无法完成业务执行形成活锁。四、饥饿Starvation线程永久等待4.1 饥饿定义与产生条件饥饿是指线程因优先级过低、锁竞争策略不合理长期无法获取CPU执行权或锁资源导致业务永久无法执行的现象。饥饿产生核心原因线程优先级设置不合理高优先级线程持续抢占资源非公平锁导致低优先级线程长期无法获取锁线程长时间持有锁不释放。4.2 饥饿流程图解4.3 饥饿实战代码案例package com.jam.demo.starvation; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.locks.ReentrantLock; /** * 饥饿示例代码 * author ken */ Slf4j public class StarvationDemo { //非公平锁高优先级线程会抢占锁低优先级线程产生饥饿 private static final ReentrantLock LOCK new ReentrantLock(false); /** * 线程执行业务逻辑 * param threadName 线程名称 * param priority 线程优先级 */ private static void task(String threadName, int priority) { Thread.currentThread().setPriority(priority); while (true) { try { LOCK.lock(); log.info(线程{}获取锁执行业务, threadName); //模拟长耗时业务 Thread.sleep(10); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error(线程中断, e); } finally { LOCK.unlock(); } } } public static void main(String[] args) { //高优先级线程 new Thread(() - task(High-Thread, Thread.MAX_PRIORITY), High-Thread).start(); //低优先级线程长期获取不到锁产生饥饿 new Thread(() - task(Low-Thread, Thread.MIN_PRIORITY), Low-Thread).start(); } }代码说明非公平锁线程优先级差异低优先级线程长期无法获取锁形成饥饿。五、死锁、活锁、饥饿核心区别与易混淆点解析5.1 核心维度对比线程状态死锁BLOCKED活锁RUNNABLE饥饿WAITING资源消耗死锁不消耗CPU活锁CPU使用率100%饥饿低消耗恢复方式死锁无法自动恢复活锁可通过随机等待恢复饥饿可通过公平锁解决检测难度死锁可工具检测活锁无明确检测工具饥饿日志可排查。5.2 关键易混淆点活锁≠死锁活锁线程在运行死锁线程阻塞饥饿≠死锁饥饿有机会获取资源死锁永久无机会公平锁能解决饥饿但会降低性能非公平锁吞吐量更高但存在饥饿风险。六、并发问题排查工具全解6.1 JDK自带基础工具6.1.1 jps查看Java进程ID命令jps -l作用定位目标Java进程是所有排查工具的基础。6.1.2 jstack线程堆栈分析死锁核心工具命令jstack pid死锁特征日志Found one Java-level deadlock: Thread-1: waiting to lock monitor 0x000002, which is held by Thread-2 Thread-2: waiting to lock monitor 0x000001, which is held by Thread-1作用直接打印死锁线程、持有锁、等待锁信息定位死锁位置。6.1.3 jconsole可视化监控工具启动方式命令行输入jconsole连接目标进程。 功能查看线程状态、锁持有情况可视化检测死锁。6.1.4 jvisualvm全能可视化工具功能线程dump、CPU监控、内存分析支持实时检测活锁、死锁。6.2 生产环境高级排查工具6.2.1 Arthas阿里开源Java诊断工具安装命令curl -O https://arthas.aliyun.com/arthas-boot.jar java -jar arthas-boot.jar核心命令thread查看所有线程状态thread -b直接定位死锁线程thread -i 1000统计CPU使用率排查活锁。6.2.2 PrometheusGrafana监控告警通过自定义线程指标死锁数量、阻塞线程数、CPU使用率实现实时告警提前发现并发问题。七、三类问题解决方案7.1 死锁解决方案破坏四大必要条件死锁无法自动恢复只能提前预防核心思路破坏死锁四大必要条件中的任意一个。7.1.1 统一锁获取顺序破坏循环等待条件修改死锁代码让所有线程按照固定顺序获取锁package com.jam.demo.solution; import lombok.extern.slf4j.Slf4j; /** * 死锁解决方案统一锁顺序 * author ken */ Slf4j public class DeadLockSolution { private static final Object LOCK_A new Object(); private static final Object LOCK_B new Object(); /** * 统一按照LOCK_A - LOCK_B的顺序获取锁 */ private static void safeMethod() { synchronized (LOCK_A) { log.info(获取LOCK_A); try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } synchronized (LOCK_B) { log.info(获取LOCK_B执行业务); } } } public static void main(String[] args) { new Thread(DeadLockSolution::safeMethod, Thread-1).start(); new Thread(DeadLockSolution::safeMethod, Thread-2).start(); } }7.1.2 使用定时锁破坏请求与保持条件使用ReentrantLock.tryLock(timeout)获取不到锁则放弃并释放已有锁private static void tryLockMethod() { Lock lockA new ReentrantLock(); Lock lockB new ReentrantLock(); try { if (lockA.tryLock(1, TimeUnit.SECONDS)) { try { if (lockB.tryLock(1, TimeUnit.SECONDS)) { log.info(成功获取所有锁); } } finally { lockB.unlock(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { lockA.unlock(); } }7.1.3 数据库死锁解决方案统一SQL执行顺序缩短事务执行时间设置事务超时时间避免长事务。7.2 活锁解决方案活锁核心解决思路打破线程互相谦让的逻辑。增加随机等待时间线程释放资源后随机休眠一段时间再争抢固定资源持有策略线程获取资源后必须完成业务再释放使用公平锁避免线程频繁切换资源。活锁修复代码//在活锁代码中添加随机等待 Thread.sleep(new Random().nextInt(100));7.3 饥饿解决方案使用公平锁new ReentrantLock(true)按照请求顺序分配锁合理设置线程优先级避免极端优先级设置减少锁持有时间拆分锁粒度使用同步代码块替代同步方法使用线程池合理控制线程数量避免线程过度竞争。饥饿修复代码//使用公平锁解决饥饿 private static final ReentrantLock FAIR_LOCK new ReentrantLock(true);八、生产环境并发编程最佳实践最小锁原则锁的粒度尽可能小只锁共享资源避免嵌套锁减少同步代码块嵌套降低死锁风险使用并发工具类优先使用java.util.concurrent包下的线程安全工具ConcurrentHashMap、CountDownLatch等定时线程监控通过定时任务打印线程堆栈提前发现异常设置超时机制所有锁获取、资源请求都添加超时时间代码评审重点审核并发代码的锁设计、资源竞争逻辑。九、总结死锁、活锁、饥饿是Java并发编程的核心难点三者的产生原理、表现形式、解决方案完全不同死锁是互相阻塞需通过锁顺序、定时锁预防活锁是无效运行需通过随机等待、固定资源策略解决饥饿是长期等待需通过公平锁、合理线程优先级解决。掌握三类问题的原理、排查工具、解决方案是开发高稳定、高并发Java应用的核心能力。在实际开发中遵循并发编程最佳实践从设计层面规避问题远比事后排查修复更高效。

更多文章