刀塔归来
119.89M · 2026-04-17
在高并发、分布式的Java应用架构中,线程安全是保障系统稳定性的核心要素,而死锁、活锁、饥饿是并发编程中最隐蔽、最棘手的三大问题。这类问题一旦在生产环境触发,会直接导致服务卡顿、线程阻塞、资源耗尽,甚至引发系统雪崩。
Java多线程通过共享内存实现数据交互,为了保证共享资源的原子性、可见性、有序性,必须使用锁机制(synchronized、ReentrantLock等)进行同步控制。
锁的本质是互斥访问:同一时间只有一个线程能持有锁,其他线程进入阻塞状态,等待锁释放。但不合理的锁设计、资源竞争策略,会直接引发死锁、活锁、饥饿问题。
| 问题类型 | 核心特征 | 线程状态 | 资源占用 |
|---|---|---|---|
| 死锁 | 线程互相持有对方需要的锁,永久阻塞 | BLOCKED | 永久占用资源 |
| 活锁 | 线程不断释放锁又重新争抢,无法执行业务 | RUNNABLE | 持续消耗CPU |
| 饥饿 | 低优先级线程长期获取不到锁,无法执行 | WAITING | 资源被高优先级线程独占 |
死锁是指两个或多个线程在执行过程中,因互相持有对方需要的资源,且都不释放自身持有的资源,导致永久阻塞的现象。
根据《Java并发编程实战》权威定义,死锁必须同时满足四个必要条件,缺一不可:
基于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,同时请求对方的锁,满足死锁四大条件,运行后程序永久阻塞。
分布式场景中,数据库行锁死锁是高频问题,示例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的行锁,互相请求对方资源,形成数据库死锁。
活锁是指线程没有阻塞,始终处于RUNNABLE状态,不断释放锁并重新争抢,却无法执行业务逻辑的现象。
与死锁不同,活锁线程会持续占用CPU资源,CPU使用率会飙升,但业务完全无法推进。
活锁产生核心原因:线程之间互相谦让资源,没有一个线程能稳定持有锁执行任务。
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();
}
}
代码说明:两个线程不断获取并主动释放资源,始终无法完成业务执行,形成活锁。
饥饿是指线程因优先级过低、锁竞争策略不合理,长期无法获取CPU执行权或锁资源,导致业务永久无法执行的现象。
饥饿产生核心原因:
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();
}
}
代码说明:非公平锁+线程优先级差异,低优先级线程长期无法获取锁,形成饥饿。
命令:
jps -l
作用:定位目标Java进程,是所有排查工具的基础。
命令:
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
作用:直接打印死锁线程、持有锁、等待锁信息,定位死锁位置。
启动方式:命令行输入jconsole,连接目标进程。 功能:查看线程状态、锁持有情况,可视化检测死锁。
功能:线程dump、CPU监控、内存分析,支持实时检测活锁、死锁。
安装命令:
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
核心命令:
thread:查看所有线程状态;thread -b:直接定位死锁线程;thread -i 1000:统计CPU使用率,排查活锁。通过自定义线程指标(死锁数量、阻塞线程数、CPU使用率)实现实时告警,提前发现并发问题。
死锁无法自动恢复,只能提前预防,核心思路:破坏死锁四大必要条件中的任意一个。
修改死锁代码,让所有线程按照固定顺序获取锁:
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();
}
}
使用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();
}
}
活锁核心解决思路:打破线程互相谦让的逻辑。
活锁修复代码:
//在活锁代码中添加随机等待
Thread.sleep(new Random().nextInt(100));
new ReentrantLock(true),按照请求顺序分配锁;饥饿修复代码:
//使用公平锁解决饥饿
private static final ReentrantLock FAIR_LOCK = new ReentrantLock(true);
java.util.concurrent包下的线程安全工具(ConcurrentHashMap、CountDownLatch等);死锁、活锁、饥饿是Java并发编程的核心难点,三者的产生原理、表现形式、解决方案完全不同:
掌握三类问题的原理、排查工具、解决方案,是开发高稳定、高并发Java应用的核心能力。在实际开发中,遵循并发编程最佳实践,从设计层面规避问题,远比事后排查修复更高效。