文章目录
📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌
📙 作者: 编程技术圈(哇哥面试陪跑)
👉 欢迎关注、分享、评论
✔️ 持续分享更多干货内容
🌐🌏🌎➕tcmeta, 欢迎沟通交流
📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌
零、引入
王二的脸比过期的馒头还白,手指在键盘上戳了半天,屏幕上的日志却像冻住的流水 —— 三个 “统计线程” 窝在角落不动弹,堆积的订单数据快把数据库表撑得冒尖。“明明给了线程优先级,咋高优先级的‘处理线程’占着 CPU 不撒手,低优先级的统计线程连口汤都喝不上?” 他扯着嗓子喊,惊得隔壁哇哥手里的搪瓷缸子晃出半杯茶。
哇哥呷了口茶,茶叶梗在水面上打转,慢悠悠道:“这不是线程偷懒,是你把它们饿坏了 —— 就像食堂里让壮汉抢完所有馒头,瘦小子只能饿着肚子蹲墙角,时间长了谁还愿意给你干活?”
点赞 + 关注,跟着哇哥把线程饥饿的病根挖透,5 招就能让每个线程都有 “饭吃”,下次再写并发代码,保准不会再出这种 “饿肚子” 的乱子。

一、王二的 “饿肚子” 代码:高优先级线程的 “霸权主义”

王二写的是电商订单系统,用三个线程处理订单,两个线程统计销量 —— 他觉得 “处理订单” 更重要,就把处理线程的优先级设成最高,统计线程设成最低。代码看着没毛病,跑起来却出了幺蛾子:
package cn.tcmeta.juc;
import java.util.concurrent.TimeUnit;
/**
* @author: laoren
* @date: 2025/12/18 12:26
* @description: 王二的坑:线程优先级设太偏,低优先级线程饿肚子
* @version: 1.0.0
*/
public class ThreadStarvationSample {
// 订单计数器(原子类保证线程安全)
private static int orderCount = 0;
// 销量统计器
private static int salesCount = 0;
static void main() {
// 1. 高优先级:订单处理线程(3个)
for (int i = 0; i < 3; i++) {
Thread processThread = new Thread(() -> {
while (true) {
// 模拟处理订单:每次+1,耗时10ms
orderCount++;
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
// 每处理100单打印一次
if (orderCount % 100 == 0) {
System.out.println(Thread.currentThread().getName() + ":处理订单" + orderCount + "单");
}
}
}, "Process-Thread-" + i);
// 设为最高优先级(10)
processThread.setPriority(Thread.MAX_PRIORITY);
processThread.start();
}
// 2. 低优先级:销量统计线程(2个)
for (int i = 0; i < 2; i++) {
Thread statThread = getThread(i);
statThread.start();
}
}
private static Thread getThread(int i) {
Thread statThread = new Thread(() -> {
while (true) {
// 模拟统计销量:每次+1,耗时5ms
salesCount++;
try {
TimeUnit.MILLISECONDS.sleep(5);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
// 每统计50次打印一次(理论上比处理线程打印更频繁)
if (salesCount % 50 == 0) {
System.out.println("【统计线程】:统计销量" + salesCount + "次");
}
}
}, "Stat-Thread-" + i);
// 设为最低优先级(1)
statThread.setPriority(Thread.MIN_PRIORITY);
return statThread;
}
}

【注意事项】: 如果cpu资源充足,则测试结果可能会不是特别明显.
王二盯着日志发呆:“统计线程明明耗时更短,咋干活这么慢?”
哇哥用手指敲了敲屏幕:“这就是线程饥饿 —— 低优先级线程长期抢不到 CPU 时间片,就像食堂里被壮汉挤到一边的学生,连餐盘都递不出去。Java 的线程优先级是‘提示’操作系统,不是‘命令’,但你把差距拉到 10 比 1,操作系统就偏着高优先级的来,低优先级的只能捡漏。”
他顿了顿,又道:“更糟的是,你这处理线程是死循环,一旦占了 CPU,就像霸着餐桌不挪窝,统计线程只能饿着。”
二、用 “食堂打饭” 讲透线程饥饿:不是不干活,是没机会干活

哇哥拽过王二桌上的草稿纸,画了个歪歪扭扭的食堂窗口,旁边标着 “CPU 时间片”,三个壮汉堵在窗口,两个瘦小子蹲在墙角 —— 这是他的拿手好戏,再玄乎的技术,到他手里都能变成街头巷尾的事儿。
“线程饥饿,说白了就是‘有活干,没资源’,” 哇哥指着草稿纸,“就像这食堂,窗口(CPU)就一个,壮汉(高优先级线程)一直抢着打饭,瘦小子(低优先级线程)连窗口都挨不着,不是不想打饭,是没机会。”
他擦掉草稿纸,又画了三个场景:
- 优先级倒置:壮汉插队,瘦小子永远排最后;
- 锁霸占:有人打了饭不离开窗口,抱着碗在那吃,后面的人只能等;
- 永久阻塞:瘦小子被人堵在食堂门外,根本进不来。
“这就是线程饥饿的三大病根,” 哇哥总结,“王二你犯的是第一类 —— 优先级设得太极端;还有人用 synchronized 锁的时候,在锁里写个死循环,那就是第二类‘锁霸占’;用 Thread.join () 不设超时时间,就是第三类‘永久阻塞’。”
👉 线程饥饿核心定义(王二记在烟盒内侧)
王二掏出皱巴巴的烟盒,用铅笔歪歪扭扭地写:
线程饥饿:线程因长期无法获取所需资源(CPU 时间片、锁、IO 等),导致任务无法推进的现象。它不是线程死锁(互相等),也不是线程阻塞(暂时等),而是 “永久或长期没机会”—— 就像饿肚子的人,不是不想吃饭,是根本没饭吃。

三、5 招根治线程饥饿:从 “霸餐” 到 “分餐” 的蜕变

哇哥拿过王二的鼠标,说:“要让线程不饿肚子,就得打破‘霸权主义’,搞‘分餐制’。这 5 招下去,保证每个线程都有饭吃。”
➡️ 第 1 招:抛弃 “极端优先级”,改用 “平等调度”
Java 的线程优先级(1-10)只是给操作系统的 “建议”,不是 “命令”,极端优先级只会加剧饥饿。解决方法是**:不用手动设优先级,让线程平等竞争 —— 就像食堂里大家排队,不分壮汉瘦小子。**
- 优化代码 1:移除优先级设置
package cn.tcmeta.juc;
import java.util.concurrent.TimeUnit;
/**
* @author: laoren
* @date: 2025/12/18 13:18
* @description: 优化点:删除所有setPriority代码,线程默认优先级5
* @version: 1.0.0
*/
public class ThreadStarvationFix1 {
private static int orderCount = 0;
private static int salesCount = 0;
static void main() {
// 订单处理线程:不设优先级
for (int i = 0; i < 3; i++) {
Thread processThread = new Thread(() -> {
while (true) {
orderCount++;
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
if (orderCount % 100 == 0) {
System.out.println(Thread.currentThread().getName() + ":处理订单" + orderCount + "单");
}
}
}, "Process-Thread-" + i);
// 移除setPriority
processThread.start();
}
// 销量统计线程:不设优先级
for (int i = 0; i < 2; i++) {
Thread statThread = new Thread(() -> {
while (true) {
salesCount++;
try {
TimeUnit.MILLISECONDS.sleep(5);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
if (salesCount % 50 == 0) {
System.out.println("【统计线程】:统计销量" + salesCount + "次");
}
}
}, "Stat-Thread-" + i);
// 移除setPriority
statThread.start();
}
}
}
运行结果:统计线程 “吃饱饭” 了

王二眼睛亮了:“统计线程终于正常了!原来优先级是个坑。”
“不是优先级是坑,是你把它用成了坑,” 哇哥说,“除非有明确的实时需求(比如嵌入式开发),否则别手动改优先级 ——Java 线程调度本来就公平,你瞎插手反而乱套。”
📢 第 2 招:用 “公平锁” 代替 “非公平锁”,拒绝 “锁插队”
synchronized 和 ReentrantLock 默认都是 “非公平锁”—— 线程释放锁时,等待队列里的线程和新线程会 “抢锁”,新线程容易插队,导致等待队列里的线程饿肚子。解决方法是用 ReentrantLock 的 “公平锁”,让线程排队,谁等得久谁先拿锁。
- 反例:非公平锁导致的饥饿
package cn.tcmeta.juc;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author: laoren
* @date: 2025/12/18 13:20
* @description: 非公平锁:新线程插队,等待线程饥饿
* @version: 1.0.0
*/
public class UnfairLockStarvation {
// 默认非公平锁
private static final Lock UNFAIR_LOCK = new ReentrantLock();
private static int count = 0;
static void main() {
// 1. 先启动一个“长占锁”线程
Thread longLockThread = new Thread(() -> {
while (true) {
UNFAIR_LOCK.lock();
try {
count++;
// 占锁100ms,模拟长耗时操作
TimeUnit.MILLISECONDS.sleep(100);
if (count % 10 == 0) {
System.out.println("长占锁线程:count=" + count);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
} finally {
UNFAIR_LOCK.unlock();
}
}
}, "Long-Lock-Thread");
longLockThread.start();
// 2. 启动5个“等待锁”线程
for (int i = 0; i < 5; i++) {
Thread waitThread = new Thread(() -> {
while (true) {
UNFAIR_LOCK.lock();
try {
count++;
System.out.println(Thread.currentThread().getName() + ":抢到锁,count=" + count);
} finally {
UNFAIR_LOCK.unlock();
}
try {
// 抢完锁歇50ms,给别人机会(但非公平锁还是会被插队)
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}, "Wait-Thread-" + i);
waitThread.start();
}
}
}
运行结果:等待线程抢不到锁(饥饿)

优化代码 2:改用公平锁
package cn.tcmeta.juc;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author: laoren
* @date: 2025/12/18 13:23
* @description: 优化点:ReentrantLock(true)启用公平锁
* @version: 1.0.0
*/
public class FairLockFix {
// 公平锁:按等待顺序分配锁
private static final Lock FAIR_LOCK = new ReentrantLock(true);
private static int count = 0;
static void main() {
// 长占锁线程(逻辑不变)
Thread longLockThread = new Thread(() -> {
while (true) {
FAIR_LOCK.lock();
try {
count++;
TimeUnit.MILLISECONDS.sleep(100);
if (count % 10 == 0) {
System.out.println("长占锁线程:count=" + count);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
} finally {
FAIR_LOCK.unlock();
}
}
}, "Long-Lock-Thread");
longLockThread.start();
// 等待锁线程(逻辑不变)
for (int i = 0; i < 5; i++) {
Thread waitThread = new Thread(() -> {
while (true) {
FAIR_LOCK.lock();
try {
count++;
System.out.println(Thread.currentThread().getName() + ":抢到锁,count=" + count);
} finally {
FAIR_LOCK.unlock();
}
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}, "Wait-Thread-" + i);
waitThread.start();
}
}
}
运行结果:等待线程按顺序抢锁
Wait-Thread-0:抢到锁,count=1
Wait-Thread-1:抢到锁,count=2
长占锁线程:count=10
Wait-Thread-2:抢到锁,count=11
Wait-Thread-3:抢到锁,count=12
哇哥解释:“公平锁就像食堂窗口贴了‘按顺序排队’的牌子,谁先来谁先打饭,不会让新到的人插队 —— 虽然效率比非公平锁略低,但能保证每个线程都有机会。”
✔️ 第 3 招:避免 “锁霸占”,拆分长耗时操作
线程持有锁时做长耗时操作(比如 IO、循环计算),会让其他线程长期等不到锁,导致饥饿。解决方法是:把长耗时操作移出锁外,让锁 “快拿快放”—— 就像打饭时,先把碗递过去打饭,拿到饭后找地方吃,别堵在窗口。
反例:锁内做长耗时操作
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
// 锁内做长耗时操作,导致其他线程饥饿
public class LongTaskInLock {
private static final Lock LOCK = new ReentrantLock();
private static String data = "";
public static void main(String[] args) {
// 长任务线程:锁内做IO操作(耗时)
Thread longTaskThread = new Thread(() -> {
while (true) {
LOCK.lock();
try {
System.out.println("长任务线程:开始获取数据(锁内)");
// 模拟网络IO(长耗时),本应移出锁外
TimeUnit.SECONDS.sleep(3);
data = "新数据-" + System.currentTimeMillis();
System.out.println("长任务线程:数据获取完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
} finally {
LOCK.unlock();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}, "Long-Task-Thread");
longTaskThread.start();
// 短任务线程:抢锁查数据,却一直等
Thread shortTaskThread = new Thread(() -> {
while (true) {
LOCK.lock();
try {
System.out.println("短任务线程:查到数据=" + data);
} finally {
LOCK.unlock();
}
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}, "Short-Task-Thread");
shortTaskThread.start();
}
}
优化代码 :长耗时操作移出锁外
// 优化点:锁内只做核心操作,长耗时操作移到锁外
public class SplitLongTaskFix {
private static final Lock LOCK = new ReentrantLock();
private static String data = "";
public static void main(String[] args) {
Thread longTaskThread = new Thread(() -> {
while (true) {
String tempData = "";
// 1. 长耗时操作:移出锁外(无锁)
System.out.println("长任务线程:开始获取数据(无锁)");
try {
TimeUnit.SECONDS.sleep(3);
tempData = "新数据-" + System.currentTimeMillis();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
// 2. 核心操作:锁内只做赋值(快拿快放)
LOCK.lock();
try {
data = tempData;
System.out.println("长任务线程:数据更新完成");
} finally {
LOCK.unlock();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}, "Long-Task-Thread");
longTaskThread.start();
// 短任务线程(逻辑不变)
Thread shortTaskThread = new Thread(() -> {
while (true) {
LOCK.lock();
try {
System.out.println("短任务线程:查到数据=" + data);
} finally {
LOCK.unlock();
}
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}, "Short-Task-Thread");
shortTaskThread.start();
}
}
运行结果:短任务线程不再饥饿
长任务线程:开始获取数据(无锁)
短任务线程:查到数据=
短任务线程:查到数据=
短任务线程:查到数据=
长任务线程:数据更新完成
短任务线程:查到数据=新数据-1678901234567
❓ 第 4 招:用 “线程池” 管理线程,避免 “无节制创建”
王二之前手动创建线程,导致线程太多,CPU 调度不过来,低优先级线程饥饿。解决方法是用线程池 —— 线程池会复用线程,控制并发数,让每个线程都有公平的调度机会,就像食堂里的 “取号机”,不会让太多人挤在窗口。
线程池替代手动创建线程
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
// 用线程池管理线程,避免饥饿
public class ThreadPoolFix {
private static int orderCount = 0;
private static int salesCount = 0;
public static void main(String[] args) {
// 1. 创建线程池:核心线程5个,足够处理任务
ExecutorService threadPool = Executors.newFixedThreadPool(5);
// 2. 提交3个订单处理任务
for (int i = 0; i < 3; i++) {
threadPool.submit(() -> {
while (!Thread.currentThread().isInterrupted()) {
orderCount++;
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
if (orderCount % 100 == 0) {
System.out.println(Thread.currentThread().getName() + ":处理订单" + orderCount + "单");
}
}
});
}
// 3. 提交2个销量统计任务
for (int i = 0; i < 2; i++) {
threadPool.submit(() -> {
while (!Thread.currentThread().isInterrupted()) {
salesCount++;
try {
TimeUnit.MILLISECONDS.sleep(5);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
if (salesCount % 50 == 0) {
System.out.println("【统计线程】:统计销量" + salesCount + "次");
}
}
});
}
// 4. 优雅关闭(实际生产环境需处理)
Runtime.getRuntime().addShutdownHook(new Thread(threadPool::shutdownNow));
}
}
哇哥补充:“线程池的核心是‘复用’和‘控制’—— 不会像手动创建线程那样,100 个线程抢 CPU,导致部分线程饿死;而且线程池的调度是公平的,每个任务都有机会被执行。”
🔔 第 5 招:给阻塞操作加 “超时时间”,拒绝 “永久等待”
用Thread.join()、Object.wait()、BlockingQueue.take()等方法时,如果不设超时时间,线程会永久阻塞,导致饥饿。解决方法是用带超时的重载方法,比如join(1000)、wait(1000),让线程 “等不及就走”,不会一直耗着。
阻塞操作加超时
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
// 阻塞操作加超时,避免永久等待
public class TimeoutFix {
private static final BlockingQueue<String> QUEUE = new ArrayBlockingQueue<>(5);
public static void main(String[] args) {
// 生产者线程:10秒后才放数据
Thread producer = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(10);
QUEUE.put("订单数据");
System.out.println("生产者:放入订单数据");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}, "Producer-Thread");
producer.start();
// 消费者线程:带超时获取数据(不会永久等待)
Thread consumer = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
// 带超时获取:3秒没数据就超时
String data = QUEUE.poll(3, TimeUnit.SECONDS);
if (data != null) {
System.out.println("消费者:拿到数据=" + data);
} else {
System.out.println("消费者:等待超时,先干别的活");
// 超时后做其他任务,避免饥饿
doOtherWork();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}, "Consumer-Thread");
consumer.start();
}
// 超时后执行的其他任务
private static void doOtherWork() {
System.out.println("消费者:执行其他任务(比如清理缓存)");
}
}
运行结果:消费者不会永久等待
消费者:等待超时,先干别的活
消费者:执行其他任务(比如清理缓存)
消费者:等待超时,先干别的活
消费者:执行其他任务(比如清理缓存)
生产者:放入订单数据
消费者:拿到数据=订单数据
四、面试必问:线程饥饿核心题(附答案)
哇哥知道王二要面试,特意整理了 3 道高频题,让他抄在小本本上,像考前划重点:

💯 面试题 1:什么是线程饥饿?它和死锁、阻塞有什么区别?
用 “食堂场景” 答,面试官一听就懂:
- 线程饥饿:线程长期拿不到资源(CPU、锁),没机会执行 —— 就像学生一直被插队,没饭吃;
- 死锁:线程互相等待对方的资源,谁都动不了 —— 就像两个学生互相抢对方的餐盘,都吃不上饭;
- 阻塞:线程暂时等待资源(比如 wait、sleep),资源到位后会继续执行 —— 就像学生排队打饭,暂时没轮到,但迟早能吃上。
核心区别:饥饿是 “长期没机会”,死锁是 “互相等”,阻塞是 “暂时等”。
✔️ 面试题 2:导致线程饥饿的常见原因有哪些?
三大病根,对应三大场景:
- 优先级倒置:低优先级线程抢不过高优先级线程,长期得不到 CPU 时间片;
- 锁资源霸占:线程持有锁后做长耗时操作,或死循环不释放锁,其他线程长期等不到锁;
- 永久阻塞:线程调用无超时的阻塞方法(join ()、wait ()、take ()),永久等待资源。
🔥 面试题 3:如何防止线程饥饿?(必答,分点说)
记住 “5 招根治法”,结合场景答:
- 平等调度:不手动设置极端线程优先级,让线程公平竞争;
- 公平锁机制:用 ReentrantLock (true) 实现公平锁,按等待顺序分配锁,避免插队;
- 锁快拿快放:把长耗时操作移出锁外,只在锁内做核心操作(如赋值、修改);
- 线程池管理:用线程池复用线程、控制并发数,避免无节制创建线程导致调度失衡;
- 阻塞加超时:阻塞操作(join、wait、poll)用带超时的重载方法,避免永久等待。
五、总结:线程饥饿根治心法(王二编的顺口溜)
王二把 5 招编成顺口溜,贴在键盘上,生怕再忘:
- 优先级别乱设,平等调度才和谐;
- 公平锁来排队,谁先等的谁先会;
- 锁内任务要简短,快拿快放别偷懒;
- 线程池来管线程,调度公平不添乱;
- 阻塞操作加超时,别让线程空等待。
哇哥看着王二把优化后的代码部署上线,日志里统计线程和处理线程各司其职,终于点了点头,说出那句收尾的话:
哇哥说:“线程如人,饿极了便不会再为你出力;所谓并发,从来不是让强者通吃,而是让每个线程都有碗饭吃 —— 这才是写代码的良心,也是搞并发的根本。你若只盯着‘高效’,忘了‘公平’,代码早晚会给你颜色看。”
关注我,下次咱们扒一扒 “线程调度的底层原理”—— 从 Java 线程到操作系统内核,带你看透 CPU 是如何给线程 “分饭” 的,让你从根上理解并发问题!

394

被折叠的 条评论
为什么被折叠?



