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

简介: 本文深入剖析Java并发中三大顽疾:死锁(线程永久阻塞)、活锁(线程忙等无效运行)、饥饿(低优先级线程长期得不到资源)。厘清其本质区别、触发条件、实战案例及jstack/Arthas等排查方案,并给出统一锁序、定时锁、公平锁等落地解决策略。

前言

在高并发、分布式的Java应用架构中,线程安全是保障系统稳定性的核心要素,而死锁、活锁、饥饿是并发编程中最隐蔽、最棘手的三大问题。这类问题一旦在生产环境触发,会直接导致服务卡顿、线程阻塞、资源耗尽,甚至引发系统雪崩。

一、并发线程基础概念回顾

1.1 线程与锁的核心关系

Java多线程通过共享内存实现数据交互,为了保证共享资源的原子性、可见性、有序性,必须使用锁机制(synchronized、ReentrantLock等)进行同步控制。

锁的本质是互斥访问:同一时间只有一个线程能持有锁,其他线程进入阻塞状态,等待锁释放。但不合理的锁设计、资源竞争策略,会直接引发死锁、活锁、饥饿问题。

1.2 三类问题的核心区别

问题类型 核心特征 线程状态 资源占用
死锁 线程互相持有对方需要的锁,永久阻塞 BLOCKED 永久占用资源
活锁 线程不断释放锁又重新争抢,无法执行业务 RUNNABLE 持续消耗CPU
饥饿 低优先级线程长期获取不到锁,无法执行 WAITING 资源被高优先级线程独占

二、死锁(Deadlock):并发编程头号杀手

2.1 死锁的官方定义与产生条件

死锁是指两个或多个线程在执行过程中,因互相持有对方需要的资源,且都不释放自身持有的资源,导致永久阻塞的现象

根据《Java并发编程实战》权威定义,死锁必须同时满足四个必要条件,缺一不可:

  1. 互斥条件:资源同一时间只能被一个线程持有;
  2. 请求与保持条件:线程持有已获取的资源,同时请求其他资源;
  3. 不可剥夺条件:线程持有的资源只能主动释放,无法被其他线程抢占;
  4. 循环等待条件:多个线程形成资源请求的环形链。

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_ALOCK_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;

原理:两个事务分别持有id=1、id=2的行锁,互相请求对方资源,形成数据库死锁。

三、活锁(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执行权或锁资源,导致业务永久无法执行的现象

饥饿产生核心原因:

  1. 线程优先级设置不合理,高优先级线程持续抢占资源;
  2. 非公平锁导致低优先级线程长期无法获取锁;
  3. 线程长时间持有锁不释放。

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 核心维度对比

  1. 线程状态:死锁=BLOCKED,活锁=RUNNABLE,饥饿=WAITING;
  2. 资源消耗:死锁不消耗CPU,活锁CPU使用率100%,饥饿低消耗;
  3. 恢复方式:死锁无法自动恢复,活锁可通过随机等待恢复,饥饿可通过公平锁解决;
  4. 检测难度:死锁可工具检测,活锁无明确检测工具,饥饿日志可排查。

5.2 关键易混淆点

  1. 活锁≠死锁:活锁线程在运行,死锁线程阻塞;
  2. 饥饿≠死锁:饥饿有机会获取资源,死锁永久无机会;
  3. 公平锁能解决饥饿,但会降低性能:非公平锁吞吐量更高,但存在饥饿风险。

六、并发问题排查工具全解

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

核心命令:

  1. thread:查看所有线程状态;
  2. thread -b:直接定位死锁线程;
  3. thread -i 1000:统计CPU使用率,排查活锁。

6.2.2 Prometheus+Grafana:监控告警

通过自定义线程指标(死锁数量、阻塞线程数、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 数据库死锁解决方案

  1. 统一SQL执行顺序;
  2. 缩短事务执行时间;
  3. 设置事务超时时间;
  4. 避免长事务。

7.2 活锁解决方案

活锁核心解决思路:打破线程互相谦让的逻辑

  1. 增加随机等待时间:线程释放资源后,随机休眠一段时间再争抢;
  2. 固定资源持有策略:线程获取资源后,必须完成业务再释放;
  3. 使用公平锁:避免线程频繁切换资源。

活锁修复代码:

//在活锁代码中添加随机等待
Thread.sleep(new Random().nextInt(100));

7.3 饥饿解决方案

  1. 使用公平锁new ReentrantLock(true),按照请求顺序分配锁;
  2. 合理设置线程优先级:避免极端优先级设置;
  3. 减少锁持有时间:拆分锁粒度,使用同步代码块替代同步方法;
  4. 使用线程池:合理控制线程数量,避免线程过度竞争。

饥饿修复代码:

//使用公平锁解决饥饿
private static final ReentrantLock FAIR_LOCK = new ReentrantLock(true);

八、生产环境并发编程最佳实践

  1. 最小锁原则:锁的粒度尽可能小,只锁共享资源;
  2. 避免嵌套锁:减少同步代码块嵌套,降低死锁风险;
  3. 使用并发工具类:优先使用java.util.concurrent包下的线程安全工具(ConcurrentHashMap、CountDownLatch等);
  4. 定时线程监控:通过定时任务打印线程堆栈,提前发现异常;
  5. 设置超时机制:所有锁获取、资源请求都添加超时时间;
  6. 代码评审:重点审核并发代码的锁设计、资源竞争逻辑。

九、总结

死锁、活锁、饥饿是Java并发编程的核心难点,三者的产生原理、表现形式、解决方案完全不同:

  1. 死锁是互相阻塞,需通过锁顺序、定时锁预防;
  2. 活锁是无效运行,需通过随机等待、固定资源策略解决;
  3. 饥饿是长期等待,需通过公平锁、合理线程优先级解决。

掌握三类问题的原理、排查工具、解决方案,是开发高稳定、高并发Java应用的核心能力。在实际开发中,遵循并发编程最佳实践,从设计层面规避问题,远比事后排查修复更高效。

目录
相关文章
|
3月前
|
缓存 Java API
从原理到落地:RAG 技术深度拆解与 Java 实战全攻略
RAG(检索增强生成)是解决大模型知识时效性差、易幻觉的核心方案:通过实时检索私有/外部数据,将其作为上下文输入大模型,实现准确、可更新的回答。相比微调,RAG成本低、上线快、维护简,是企业落地LLM的首选架构。
241 1
|
3月前
|
存储 缓存 Java
击穿 Java 高并发性能瓶颈:伪共享底层原理、缓存行填充与 @Contended 注解全维度深度拆解
本文深入剖析CPU缓存体系、MESI协议与缓存行原理,系统揭示伪共享(False Sharing)的本质、产生机制及对Java高并发性能的严重危害,并详解手动填充与JVM原生`@Contended`注解两种解决方案,涵盖检测定位、最佳实践与跨架构适配。
290 1
|
3月前
|
SQL 存储 关系型数据库
90% 的 MySQL 慢查询都栽在这!索引失效底层原理与精准修复全攻略
本文深入剖析MySQL索引失效的12大高频场景,从B+树底层原理、聚簇/二级索引结构讲起,结合EXPLAIN执行计划关键字段解读,逐一解析函数操作、隐式转换、模糊查询、最左前缀等失效原因,并提供可落地的修复方案与Java实战示例。
453 3
|
SQL 关系型数据库 数据库
学习分布式事务Seata看这一篇就够了,建议收藏
学习分布式事务Seata看这一篇就够了,建议收藏
25581 2
|
3月前
|
存储 安全 Java
ConcurrentHashMap 深度解析:从 JDK7 到 JDK8 的演进与并发安全保障
本文详解 Java 中 `ConcurrentHashMap` 的演进:JDK7 采用分段锁(Segment)提升并发性;JDK8 重构为 CAS + synchronized + 红黑树,支持并发扩容与更优查询性能。对比差异,剖析线程安全机制与使用要点。
289 12
|
4月前
|
消息中间件 缓存 NoSQL
秒杀系统高并发核心优化与落地全指南
本文系统阐述秒杀系统架构设计:剖析瞬时高并发、库存超卖等核心痛点,提出漏斗过滤、读写分离、强一致性等设计原则;详解前端、Nginx、网关、业务、缓存、消息队列及数据库七层优化方案;并给出Redis预扣减+异步落库等生产级解决方案与完整代码实现。
676 3
|
3月前
|
存储 开发框架 架构师
软考系统架构师硬核通关笔记 - 计算机系统基础
本文专为软考系统架构师考生打造,直击计算机系统基础知识备考痛点:摒弃死记硬背,深度剖析CPU(运算器/控制器、CISC/RISC、GPU/DSP/FPGA)、存储体系(SRAM/DRAM/Cache映射与计算)、I/O控制(中断/DMA/通道)、总线接口及操作系统核心原理,并贯通分布式架构(CORBA/J2EE/DNA)与备考策略,强调场景理解与底层逻辑,助你高效通关。
329 5
|
3月前
|
算法 Java 测试技术
JDK26 G1ZGC 双引擎升级:高并发应用吞吐量暴涨 真相
JDK26 GC迎来革命性升级:G1与ZGC全面重构,突破“吞吐-延迟-内存”不可能三角。通过增量标记、并发年轻代回收、NUMA优化等技术,实测吞吐提升32%+,99.9分位延迟降低71%,告别调参玄学,迈向智能自适应GC新时代。
351 7
|
3月前
|
存储 关系型数据库 MySQL
PostgreSQL vs MySQL:Java 选型指南与实战避坑
本文深度对比PostgreSQL与MySQL在Java项目中的选型差异,涵盖底层架构(插件式引擎 vs 一体化设计)、数据类型、SQL特性(JSON/CTE/窗口函数)、事务隔离、索引原理(聚簇vs非聚簇)、Java开发实践及性能基准测试,并结合互联网、金融、GIS等场景给出选型建议,助力架构师科学决策。
220 1
|
4月前
|
消息中间件 存储 Java
消息队列选型终极指南:Kafka、RocketMQ、RabbitMQ 底层原理与场景化选型全解
本文深度解析消息队列核心原理与三大主流MQ(RabbitMQ、RocketMQ、Kafka)的架构、特性、代码实现及选型策略。涵盖异步解耦、流量削峰、数据分发三大价值,At-most/least/exactly-once投递语义,推拉模式差异,事务消息实现对比,并提供场景化选型指南与生产避坑实践。
584 1