我被 Executors 坑到OOM!Java线程池入门避坑指南

115 阅读5分钟

作为刚接触并发编程的小白,我曾经以为 Executors 是线程池的救星——一行代码 Executors.newFixedThreadPool(5) 就能创建线程池,简直不要太方便!直到上周线上服务突然OOM崩溃,日志里满是 OutOfMemoryError ,我才知道这个"便捷工具"背后藏着多大的坑。今天就用我踩过的血泪教训,聊聊为啥别再用 Executors 创建线程池,以及新手该咋正确用线程池。 一、那些年 Executors 给我挖的坑

  1. newFixedThreadPool:让我服务器内存爆掉的"无底洞"

我第一次用线程池就是 Executors.newFixedThreadPool(10) ,以为固定10个线程很安全。结果高峰期任务太多,线程处理不过来,任务就堆在队列里。后来才知道它用的 LinkedBlockingQueue 默认能存20亿个任务!就像给餐厅装了个能坐20亿人的等候区,客人来了就往里塞,最后餐厅直接被挤爆(内存溢出)。

源码里明明白白写着:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>
                                  ()); // 这个队列默认大小是Integer.
                                  MAX_VALUE!
}
  1. newCachedThreadPool:创建10万个线程的"自杀式"操作

后来我想处理大量短期任务,又换了 Executors.newCachedThreadPool() 。文档说它会"按需创建线程",结果一压测直接懵了——系统疯狂创建线程,30分钟就建了5万个!每个线程占1MB栈内存,5万个就是50GB,服务器直接卡死。

看源码才发现最大线程数居然是20亿:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, // 最大线程数20亿,
    这哪是缓存,这是洪水!
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

就像开餐厅不限制服务员数量,来一个客人招一个服务员,最后服务员比客人还多,工资都发不起(系统资源耗尽)。

  1. newSingleThreadExecutor:披着单线程外衣的"内存炸弹"

最坑的是我用它处理定时任务,以为单线程肯定安全。结果任务越积越多,内存悄悄涨到几个G。原来它和 newFixedThreadPool 一样用了无界队列,就像用吸管喝奶茶,看着小口喝,其实杯子是无限大的,最后撑死自己。 二、为啥 Executors 这么坑?新手必懂的底层原因 后来问了公司大佬才明白, Executors 的问题就出在"默认参数太危险":

  • 队列没上限 : LinkedBlockingQueue 默认20亿容量,任务无限堆积=内存无限飙升
  • 线程数没上限 : Integer.MAX_VALUE 当最大线程数,高并发下直接线程爆炸
  • 拒绝策略太粗暴 :默认直接抛异常,连个缓冲都没有 就像玩游戏开了"无敌挂",前期爽得很,后期直接封号。 Executors 把复杂参数都藏起来了,表面方便,实则让新手失去了对线程池的控制权。 三、正确姿势:像搭积木一样创建 ThreadPoolExecutor 大佬说:"别用玩具积木(Executors),要用乐高(ThreadPoolExecutor)自己拼!" 直接用 ThreadPoolExecutor 的构造方法,7个参数自己配,安全感瞬间拉满。

用餐厅模型理解7个参数

我把线程池想象成一家餐厅,这7个参数就很好懂了:

参数 餐厅 analogy 新手怎么设值? corePoolSize 正式员工数量(全职,不轻易开除) CPU核心数*2(IO密集型)/ CPU核心数+1(计算密集型) maximumPoolSize 最多能招多少人(正式+兼职) 比corePoolSize大,一般不超过100(太多人反而乱) keepAliveTime 兼职员工没活干多久开除 60秒(默认值就挺合适) unit 时间单位 秒(TimeUnit.SECONDS) workQueue 候客区座位(必须设上限!) new ArrayBlockingQueue<>(1000)(最多等1000个任务) threadFactory 给员工起名字(方便查问题) 自定义线程名,比如"order-service-pool-1" handler 候客区满了怎么办 CallerRunsPolicy(让老板亲自帮忙,慢点但不丢单)

新手友好的线程池代码模板

这是我现在用的模板,注释写得明明白白,再也不怕配错:

// 1. 给线程起个好记的名字(出问题时日志里能找到爹)
ThreadFactory threadFactory = new ThreadFactory() {
    private final AtomicInteger counter = new AtomicInteger(1);
    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r, "order-service-pool-" + counter.
        getAndIncrement());
        thread.setDaemon(false); // 非守护线程,任务没跑完不会被强制终止
        return thread;
    }
};

// 2. 像搭积木一样拼线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    10// 正式员工10个(corePoolSize)
    20// 最多招20个人(maximumPoolSize)
    60L// 兼职60秒没活干就开除(keepAliveTime)
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(1000), // 候客区1000个座位(有界队列!)
    threadFactory,
    new ThreadPoolExecutor.CallerRunsPolicy() // 候客区满了让老板帮忙(拒绝策
    略)
);

四、新手避坑总结(血泪经验)

  1. 永远别用 Executors 一键创建 :就像别买"一键煮火锅"的懒人锅,看着方便,实则调料都给你配好了,想吃辣都不行
  2. 队列必须设上限 :用 ArrayBlockingQueue 而不是 LinkedBlockingQueue ,就像候客区必须有座位数限制,不然客人能排到街对面
  3. 线程数别太贪心 :最大线程数不是越大越好,就像餐厅服务员太多会打架,20-50个足够应付大部分场景
  4. 线程名字一定要自定义 :出问题时日志里全是"pool-1-thread-1",根本不知道哪个线程池炸了
  5. 拒绝策略选 CallerRunsPolicy :新手最安全的选择,任务太多时让提交任务的线程自己处理,相当于顾客太多老板亲自下厨,虽然慢点但不会丢单 现在我看到用 Executors 的代码就头大,自己写线程池虽然多几行代码,但心里踏实。希望我的踩坑经历能帮到和我一样的新手——并发编程水很深,别让"便捷工具"变成埋在系统里的定时炸弹!