线程池队列暗坑: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不设上限,就像家里收快递不关大门,快递能堆到把门口堵死,你连门都出不去。
给队列设一个合理的上限,配合合适的拒绝策略,这才是线程池的正常用法。
最后提醒一句: 写完代码后,可以模拟高流量场景测试一下。把队列塞满,看看系统表现如何。这种提前验证,比线上出问题再修复要省心得多。
感谢阅读,我是小明他不是名。如果觉得有用,欢迎转发给身边写代码的朋友。

posted @ 2026-06-10 19:46  小明他不是名  阅读(9)  评论(0)    收藏  举报