活锁是线程状态为runnable但业务无进展;饥饿是线程长期处于waiting/timed_waiting却得不到调度;死锁则表现为线程互相等待锁。三者需通过jstack输出的线程状态与堆栈精准区分。

活锁:线程状态是 RUNNABLE,但 CPU 白跑,业务没进展
活锁不是卡死,而是“假忙真停”——线程一直在执行,Thread.getState() 返回 RUNNABLE,CPU 占用不低,但关键逻辑(比如消息处理、任务提交、锁获取)反复失败、原地打转。
典型错误现象:
- 用
tryLock()获取锁失败后,立刻unlock()(其实根本没 lock 成功),再Thread.sleep(0)或固定毫秒值重试 - 分布式任务队列里,失败消息被无条件插回队首,下一轮又第一个被捞出、又失败、又插回
- 两个线程同时尝试交换资源(如转账 A→B 和 B→A),发现冲突就主动回退并重试,结果永远“你让一步、我让一步”
关键解决点是打破同步节奏:
- 重试前必须加随机延迟:
Thread.sleep(ThreadLocalRandom.current().nextInt(10, 100)),别用sleep(10)—— 多线程会迅速趋同,冲突复现率反而更高 - 每次重试都要检查中断:
if (Thread.interrupted()) throw new InterruptedException();,否则无法响应shutdownNow() - 避免在重试循环里做非幂等操作(比如发一次通知、写一次日志),否则日志刷屏却无实质推进
饥饿:线程能跑、也想跑,但永远轮不到它
饥饿的线程状态常是 WAITING 或 TIMED_WAITING,比如卡在 ReentrantLock.lock()、Object.wait()、或 LinkedBlockingQueue.take() 上,但不是因为对方死锁,而是调度/排队机制本身不公平。
常见错误场景:
- 把后台统计线程设成
Thread.MIN_PRIORITY,而前台 HTTP 请求线程占满 CPU,JVM 不保证优先级跨平台生效,该线程可能几小时不调度一次 - 用默认构造的
ReentrantLock(非公平模式),新线程总比等了 5 秒的老线程更容易抢到锁 - 用
wait()+notify()实现生产者消费者,但notify()总唤醒刚进来的线程,老等待者一直被跳过
实操建议:
- 禁用
setPriority()—— 它在 Linux/OpenJDK 上基本无效,还引入不可移植风险 - 需要排队公平性时,显式启用公平锁:
new ReentrantLock(true);但注意:吞吐量下降 15–30%,别在高频短临界区滥用 - 替代
wait()/notify():用Condition配合公平锁,或直接上java.util.concurrent的公平类,比如LinkedBlockingQueue(构造时传true启用公平模式)
怎么一眼区分活锁、饥饿、死锁?看 jstack 输出+线程状态
别靠猜。出问题第一时间跑 jstack <pid></pid> 或开 JConsole 点“检测死锁”按钮:
- 如果有死锁,JVM 会直接标出互相等待的线程和锁地址,堆栈里出现
waiting to lock和locked循环对 - 如果没有死锁,但一堆线程状态是
RUNNABLE,且堆栈反复出现在tryLock()→sleep()→ 循环,就是活锁 - 如果线程停在
parking to wait for或java.lang.Object.wait(Native Method),且等待时间远超预期(比如 >30s),大概率是饥饿
注意:WAITING 状态不等于饥饿——它可能是正常阻塞(如 CountDownLatch.await()),得结合业务逻辑和等待时长判断。
最容易被忽略的坑:活锁和饥饿都难复现,但上线后才爆发
本地压测往往看不到活锁,因为线程数少、延迟低、竞争弱;饥饿在单核机器上更隐蔽,因为调度器“假装”给了低优先级线程机会。
真实系统里,这些毛病只在以下情况集中暴露:
- 流量突增时,重试逻辑被放大,活锁从偶发变常态
- 后台任务(如定时清理、指标聚合)和前台请求共用线程池,且没做隔离,饥饿线程彻底失联
- 容器环境(K8s)限制 CPU 时间片,
Thread.MIN_PRIORITY的线程可能被 OS 层直接饿死,比 JVM 层更狠
所以,凡是有重试、有等待、有优先级或锁竞争的地方,都要问一句:它会不会“看起来在跑,其实没动”?








