线程池队列暗坑:LinkedBlockingQueue内存占用真相!
各位开发者朋友,大家好,我是小明他不是名。
今天咱们聊一个实际开发中经常遇到,但又容易被忽视的问题。很多人在用线程池时,觉得LinkedBlockingQueue这个东西挺好——不用手动设置长度,来多少任务存多少,多省心。
但跑着跑着,服务突然变慢,甚至没反应了。你去查内存,发现堆内存快用完了。怎么回事?罪魁祸首很可能就是这个“无限长”的队列。
咱们不绕弯子,直接用大白话把这个坑讲透。
一、到底什么是“内存黑洞”?
LinkedBlockingQueue如果不设置上限,理论上可以往里面塞上亿个任务。每个任务都是一个对象,都占用一块内存。
想象一下:你后端服务处理请求本来只要50毫秒,结果下游数据库慢了一下,变成5秒。这时候请求还在呼呼地进来,线程池里的工作线程处理不过来,所有新任务就只能排队。
队列长度从0涨到1000、10000、100000……每个排队任务都抱着你的请求数据不撒手。内存就这样被一点点吃光,直到触发报警。
这就像一个仓库,本来设计装1000箱货。结果大家图省事,没装限高门,货物堆到天花板、堆到大门口、堆到大街上。最后整个仓库区域都被堵死。
二、一个让你秒懂的代码示例
// 不设置队列上限的写法(隐患很大)
ExecutorService executor = new ThreadPoolExecutor(
2, // 核心线程少
5, // 最大线程少
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>() // 没有设置容量上限
);
三、复现“黑洞”效果
public class HoleDemo {
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>() // 无限队列
);
// 让工作线程一直忙
pool.submit(() -> {
while (true) {}
});
// 疯狂塞任务
for (int i = 0; i < 1_000_000; i++) {
final int id = i;
pool.submit(() -> System.out.println("任务" + id));
}
}
}
上面这段代码运行后,一亿个任务会堆积在队列里。你的内存会快速上升,直到服务失去响应。
四、正确的做法:设置合理边界 www.kfzhan.com www.99sf.com.cn www.mir-cq.com.cn
// 推荐写法:给队列一个上限
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(1000);
ThreadPoolExecutor safePool = new ThreadPoolExecutor(
5, 10, 60L, TimeUnit.SECONDS,
queue,
new ThreadPoolExecutor.CallerRunsPolicy() // 满了让调用线程自己跑
);
五、不同队列长度对内存的影响
public class MemoryCompare {
public static void main(String[] args) {
// 每个任务持有1KB数据
class HeavyTask implements Runnable {
byte[] data = new byte[1024];
public void run() {
try { Thread.sleep(10000); } catch (Exception e) {}
}
}
// 无界队列:内存占用会持续增长
LinkedBlockingQueue<Runnable> unbounded = new LinkedBlockingQueue<>();
for (int i = 0; i < 200000; i++) {
unbounded.offer(new HeavyTask()); // 约占用200MB内存
}
// 有界队列:超出上限时触发拒绝策略,内存可控
LinkedBlockingQueue<Runnable> bounded = new LinkedBlockingQueue<>(1000);
for (int i = 0; i < 200000; i++) {
boolean added = bounded.offer(new HeavyTask());
if (!added) {
System.out.println("队列满了,任务被拒绝或转入其他处理");
}
}
}
}
六、问答环节一
问:那是不是永远不能用无参的LinkedBlockingQueue?
答: 也不是绝对不能用,但得看场景。比如任务量本身是可控的,或者上游已经做了限流,那用无参队列影响不大。但如果你不确定峰值流量有多大,建议还是主动设置一个容量,这是个好习惯。
七、如何监控队列状态 www.787game.com.cn www.387game.com.cn www.zaouc.com
public class QueueMonitor {
public static void monitor(ThreadPoolExecutor pool) {
ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor();
timer.scheduleAtFixedRate(() -> {
int queueSize = pool.getQueue().size();
int activeCount = pool.getActiveCount();
long completed = pool.getCompletedTaskCount();
System.out.printf("队列长度: %d, 活跃线程: %d, 完成任务数: %d%n",
queueSize, activeCount, completed);
// 阈值告警(不触发真实报警,只打印提示)
if (queueSize > 5000) {
System.err.println("注意:队列堆积已超过5000,请关注服务状态");
}
}, 1, 1, TimeUnit.SECONDS);
}
}
八、四种拒绝策略对比
// 策略1:直接丢弃新任务(不推荐,用户会感到异常)
new ThreadPoolExecutor.DiscardPolicy();
// 策略2:丢弃队列里等待最久的任务,把新任务放进去(适合消息类场景)
new ThreadPoolExecutor.DiscardOldestPolicy();
// 策略3:抛出异常(默认行为,需要上游处理异常)
new ThreadPoolExecutor.AbortPolicy();
// 策略4:由调用线程自己执行任务(最推荐,能有效减缓任务提交速度)
new ThreadPoolExecutor.CallerRunsPolicy();
九、实战中的参数推荐配置
public class RecommendedConfig {
// 根据实际业务调整以下参数
public static ThreadPoolExecutor createSafePool() {
int coreSize = Runtime.getRuntime().availableProcessors();
int maxSize = coreSize * 2;
int queueCapacity = 2000; // 根据预估积压量设定
return new ThreadPoolExecutor(
coreSize, maxSize, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(queueCapacity),
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
}
十、如何排查线上是否存在队列堆积
// 用jstack或Arthas工具查看线程池状态
// 以下是模拟查询逻辑的代码示例
public class CheckQueue {
public static void check(ThreadPoolExecutor pool) {
int size = pool.getQueue().size();
int remaining = pool.getQueue().remainingCapacity();
if (size > 0 && remaining == 0) {
System.out.println("队列已满,部分任务正在被拒绝策略处理");
} else if (size > 10000) {
System.out.println("队列堆积严重,请检查下游服务或增加消费者");
}
}
}
十一、问答环节二
问:用有界队列是不是一定比无界队列好? www.187game.com.cn www.687game.com.cn www.71wa.com www.kkkmir.com
答: 不一定。有界队列加了限制,超过限制任务就会被处理(比如丢弃或让调用方自己跑)。这其实是一种“自我保护”——宁可丢掉一部分新请求,也不让整个服务崩溃。通常推荐这种做法,因为它把问题暴露出来,而不是悄悄藏在队列里。
十二、总结一句大白话
LinkedBlockingQueue不设上限,就像家里收快递不关大门,快递能堆到把门口堵死,你连门都出不去。
给队列设一个合理的上限,配合合适的拒绝策略,这才是线程池的正常用法。
最后提醒一句: 写完代码后,可以模拟高流量场景测试一下。把队列塞满,看看系统表现如何。这种提前验证,比线上出问题再修复要省心得多。
感谢阅读,我是小明他不是名。如果觉得有用,欢迎转发给身边写代码的朋友。

浙公网安备 33010602011771号