跳转到主内容
趣航编程网 - 趣学编程,启航技术之路!

活锁(Livelock)与饥饿:探讨线程在尝试获取变量锁时不断重复失败的尴尬

活锁是RUNNABLE状态下的无效忙碌,表现为CPU忙但无进展;饥饿是WAITING/TIMED_WAITING状态下的长期缺席,因调度不公导致线程长期得不到执行。

活锁和饥饿都表现为线程“干不了活”,但成因和表现截然不同:活锁是线程在跑、CPU在忙,却原地打转;饥饿是线程想跑却一直排不上队,长期卡在等待状态。

活锁:RUNNABLE 状态下的无效忙碌当多个线程反复尝试获取同一变量的锁(比如用tryLock()),失败后立刻重试,又不加任何退让策略,就容易陷入活锁。此时线程状态始终是RUNNABLE,jstack 能看到它在循环调用 lock/unlock 或消息重入逻辑,但业务毫无进展。

典型场景:两个转账线程同时尝试 A→B 和 B→A,发现冲突就回退并立即重试,结果永远“你让一步、我让一步”

另一个常见例子:失败消息被无条件插回队列头部,下一轮又被优先取出、再次失败、再次插回关键识别点:CPU 使用率不低,但日志里重复出现“获取锁失败”“处理超时”“重试第 N 次”等字样饥饿:WAITING/TIMED_WAITING 下的长期缺席饥饿不是锁没抢到,而是调度机制本身把某个线程“晾在一边”。比如一个后台统计线程设为Thread.MIN_PRIORITY,而前台请求线程持续占满 CPU,JVM 不保证优先级跨平台生效,该线程可能数小时得不到一次调度机会。

常见位置:卡在ReentrantLock.lock()(非公平模式)、LinkedBlockingQueue.take()、或Object.wait()上,但堆栈里没有死锁式循环等待根本原因常是资源分配不公平:高优先级线程垄断 CPU、锁获取不按排队顺序、notify() 总唤醒新来者而非等待最久者与活锁的区别很直观:jstack 中状态多为WAITING或TIMED_WAITING,且线程长时间无日志输出怎么破?关键在打破节奏与保障公平活锁要打破同步共振,饥饿要修复调度失衡。

对活锁:重试前必须加随机延迟(如ThreadLocalRandom.current().nextInt(10, 100)),避免所有线程在毫秒级重试窗口里整齐划一地撞车对活锁:每次重试检查Thread.interrupted(),确保能响应中断,否则 shutdownNow() 形同虚设对饥饿:改用new ReentrantLock(true)(公平锁),或给关键后台线程显式设置合理优先级+定期Thread.yield()对饥饿:避免依赖 notify(),改用notifyAll()或更现代的Condition.await()配合 awaitNanos() 控制等待粒度不复杂但容易忽略:活锁靠“错峰”,饥饿靠“排队”。看线程状态、查堆栈、测重试逻辑,三步就能准确定位问题类型。

相关文章