摘要

线程池是 Java 面试里非常高频的并发题。很多人能背出 corePoolSizemaximumPoolSizeworkQueue,但一问线程池执行流程、拒绝策略、为什么不建议使用 Executors,就容易答乱。本文用面试视角,结合代码案例,梳理线程池的作用、7 个核心参数、任务执行流程、常见队列、拒绝策略以及实际开发中的使用建议。


前言

Java 后端面试里,线程池基本是绕不开的。

这个题看起来是八股,但实际开发中也经常用到,比如:

  • 异步处理;
  • 批量任务;
  • 定时任务;
  • 多线程导入;
  • 报表计算;
  • 接口并发调用;
  • 消息消费加速。

很多人背线程池,喜欢直接背 7 个参数。

但面试官真正想听的,不只是参数名,而是:

线程池为什么需要这些参数?任务提交进来后到底怎么执行?队列满了怎么办?线程数什么时候增加?什么时候拒绝?

这篇就用比较直接的方式,把线程池讲清楚。


一、面试官一般怎么问?

线程池常见问法有这些:

  • 线程池是干什么的?
  • 为什么要用线程池?
  • 线程池 7 个核心参数是什么?
  • 线程池任务执行流程是什么?
  • corePoolSizemaximumPoolSize 有什么区别?
  • 常见拒绝策略有哪些?
  • 常见阻塞队列有哪些?
  • 为什么不建议使用 Executors 创建线程池?
  • 实际开发中线程池参数怎么配置?
  • 线程池使用不当会有什么问题?

二、线程池是干什么的?

一句话:

线程池是用来复用线程、控制并发数量、减少线程频繁创建和销毁成本的。

如果每来一个任务就创建一个线程:

new Thread(() -> {
    // 执行业务逻辑
}).start();

问题很明显:

  • 线程频繁创建和销毁,有性能损耗;
  • 线程数量不可控,容易把 CPU、内存打满;
  • 没有统一管理,不方便监控和关闭;
  • 任务多的时候,系统容易被拖垮。

线程池就是为了解决这些问题。

它可以把线程提前创建好,任务来了之后交给线程池执行。


三、线程池 7 个核心参数

自定义线程池一般使用 ThreadPoolExecutor

构造方法如下:

public ThreadPoolExecutor(
        int corePoolSize,
        int maximumPoolSize,
        long keepAliveTime,
        TimeUnit unit,
        BlockingQueue<Runnable> workQueue,
        ThreadFactory threadFactory,
        RejectedExecutionHandler handler
) {
    // ...
}

这 7 个参数分别是:

参数

含义

corePoolSize

核心线程数

maximumPoolSize

最大线程数

keepAliveTime

非核心线程空闲存活时间

unit

时间单位

workQueue

任务队列

threadFactory

线程工厂

handler

拒绝策略


四、corePoolSize:核心线程数

corePoolSize 表示核心线程数。

简单理解:

线程池里长期保留的基础线程数量。

比如:

corePoolSize = 5

表示线程池会优先创建 5 个核心线程来处理任务。

只要任务不断进来,核心线程会一直负责执行。

默认情况下,核心线程即使空闲,也不会被销毁。

除非设置:

threadPoolExecutor.allowCoreThreadTimeOut(true);

五、maximumPoolSize:最大线程数

maximumPoolSize 表示线程池允许创建的最大线程数。

比如:

corePoolSize = 5
maximumPoolSize = 10

意思是:

  • 平时先用 5 个核心线程处理任务;
  • 如果任务太多,队列也满了;
  • 线程池最多可以扩到 10 个线程。

需要注意:

不是任务一多,就马上创建到 maximumPoolSize。

线程池会先看核心线程,再看队列,最后才考虑创建非核心线程。

这个执行顺序很重要。


六、workQueue:任务队列

workQueue 是任务队列。

当核心线程都忙时,新任务会先进入队列等待。

常见队列有:

1. ArrayBlockingQueue

有界阻塞队列,底层是数组。

new ArrayBlockingQueue<>(100);

特点:

  • 队列长度固定;
  • 满了之后不能继续放;
  • 更容易控制内存风险。

2. LinkedBlockingQueue

链表阻塞队列。

new LinkedBlockingQueue<>(100);

也可以不指定容量,但不建议。

如果不指定容量,默认容量非常大,容易堆积大量任务,导致内存风险。


3. SynchronousQueue

不存储任务的队列。

任务提交后必须马上有线程接收,否则就创建新线程或触发拒绝策略。

newCachedThreadPool 里面就用了它。


4. PriorityBlockingQueue

优先级队列。

可以让任务按优先级执行。

但实际开发中用得相对少一些。


七、keepAliveTime 和 unit

keepAliveTime 表示非核心线程空闲多久后会被回收。

比如:

keepAliveTime = 60
unit = TimeUnit.SECONDS

意思是:

非核心线程空闲超过 60 秒,就会被销毁。

注意,一般情况下,这个参数主要作用于非核心线程。

核心线程默认不会因为空闲而销毁。


八、threadFactory:线程工厂

threadFactory 用来创建线程。

实际开发中建议自定义线程名称。

因为线上排查问题时,线程名非常重要。

例如:

ThreadFactory threadFactory = new ThreadFactory() {

    private final AtomicInteger index = new AtomicInteger(1);

    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r);
        thread.setName("order-task-pool-" + index.getAndIncrement());
        return thread;
    }
};

这样日志里能看到线程名:

order-task-pool-1
order-task-pool-2

如果线程池没有命名,线上排查会很难受。


九、handler:拒绝策略

当线程池已经达到最大线程数,并且任务队列也满了,新任务就会触发拒绝策略。

JDK 提供了 4 种常见拒绝策略。

1. AbortPolicy

默认策略,直接抛异常。

new ThreadPoolExecutor.AbortPolicy()

会抛:

RejectedExecutionException

2. CallerRunsPolicy

由提交任务的线程自己执行任务。

new ThreadPoolExecutor.CallerRunsPolicy()

这个策略不会直接丢任务。

但会让提交任务的线程变慢,相当于一种反压。


3. DiscardPolicy

直接丢弃任务,不抛异常。

new ThreadPoolExecutor.DiscardPolicy()

这个要慎用。

因为任务丢了,你可能还不知道。


4. DiscardOldestPolicy

丢弃队列中最老的任务,然后尝试提交当前任务。

new ThreadPoolExecutor.DiscardOldestPolicy()

这个也要慎用。

因为被丢弃的老任务可能也很重要。


十、线程池任务执行流程

这个是面试重点。

任务提交到线程池后,大概按这个顺序执行:

1. 如果当前线程数小于 corePoolSize,创建核心线程执行任务
2. 如果核心线程数已满,任务进入队列
3. 如果队列满了,并且当前线程数小于 maximumPoolSize,创建非核心线程执行任务
4. 如果线程数已经达到 maximumPoolSize,并且队列也满了,触发拒绝策略

一句话版本:

先核心线程,再任务队列,再最大线程,最后拒绝策略。

这个流程一定要记住。


十一、完整代码案例:自定义线程池

下面写一个完整示例。

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class CustomThreadPoolDemo {

    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2,
                4,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),
                new NamedThreadFactory("biz-task-pool"),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        for (int i = 1; i <= 10; i++) {
            int taskNo = i;

            executor.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " 执行任务:" + taskNo);

                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        executor.shutdown();
    }

    static class NamedThreadFactory implements ThreadFactory {

        private final String prefix;
        private final AtomicInteger index = new AtomicInteger(1);

        public NamedThreadFactory(String prefix) {
            this.prefix = prefix;
        }

        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            thread.setName(prefix + "-" + index.getAndIncrement());
            return thread;
        }
    }
}

这个线程池配置是:

核心线程数:2
最大线程数:4
队列容量:3
拒绝策略:CallerRunsPolicy

任务执行过程大概是:

前 2 个任务创建核心线程执行
接下来 3 个任务进入队列
队列满了之后,再创建非核心线程
线程达到最大值后,再来的任务触发拒绝策略

十二、观察线程池执行流程

为了更直观看线程池状态,可以打印线程池信息。

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadPoolStatusDemo {

    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2,
                4,
                30,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(2),
                new NamedThreadFactory("status-pool"),
                new ThreadPoolExecutor.AbortPolicy()
        );

        for (int i = 1; i <= 6; i++) {
            int taskNo = i;

            executor.execute(() -> {
                printStatus(executor, "执行任务前:" + taskNo);

                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }

                printStatus(executor, "执行任务后:" + taskNo);
            });
        }

        TimeUnit.SECONDS.sleep(10);
        executor.shutdown();
    }

    private static void printStatus(ThreadPoolExecutor executor, String message) {
        System.out.println("[" + message + "]"
                + " poolSize=" + executor.getPoolSize()
                + ", activeCount=" + executor.getActiveCount()
                + ", queueSize=" + executor.getQueue().size()
                + ", completedTaskCount=" + executor.getCompletedTaskCount());
    }

    static class NamedThreadFactory implements ThreadFactory {

        private final String prefix;
        private final AtomicInteger index = new AtomicInteger(1);

        public NamedThreadFactory(String prefix) {
            this.prefix = prefix;
        }

        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            thread.setName(prefix + "-" + index.getAndIncrement());
            return thread;
        }
    }
}

这里可以看到几个指标:

方法

含义

getPoolSize()

当前线程池线程数

getActiveCount()

正在执行任务的线程数

getQueue().size()

当前队列中等待的任务数

getCompletedTaskCount()

已完成任务数

这些指标在排查线程池问题时很有用。


十三、拒绝策略代码案例

下面演示默认拒绝策略 AbortPolicy

import java.util.concurrent.*;

public class RejectPolicyDemo {

    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                1,
                1,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(1),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );

        for (int i = 1; i <= 3; i++) {
            int taskNo = i;

            executor.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " 执行任务:" + taskNo);

                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        executor.shutdown();
    }
}

这个线程池配置是:

核心线程数:1
最大线程数:1
队列容量:1

提交 3 个任务时:

  • 第 1 个任务由线程执行;
  • 第 2 个任务进入队列;
  • 第 3 个任务无法执行,也无法入队;
  • 触发拒绝策略。

最终会抛出:

RejectedExecutionException

十四、为什么不建议使用 Executors?

JDK 提供了 Executors 工具类,可以快速创建线程池。

比如:

ExecutorService executor = Executors.newFixedThreadPool(10);

看起来很方便。

但实际开发中不建议直接使用。

原因是很多默认配置不够可控。


1. newFixedThreadPool

Executors.newFixedThreadPool(10);

底层使用的是无界队列:

LinkedBlockingQueue

如果任务提交速度远大于消费速度,队列会不断堆积,可能导致 OOM。


2. newSingleThreadExecutor

Executors.newSingleThreadExecutor();

它也是使用无界队列。

如果任务太多,同样可能堆积大量任务。


3. newCachedThreadPool

Executors.newCachedThreadPool();

它的最大线程数非常大。

如果任务非常多,可能创建大量线程,导致系统资源被打爆。


所以更推荐手动创建 ThreadPoolExecutor

这样核心线程数、最大线程数、队列长度、线程名称、拒绝策略都可以自己控制。


十五、Executors 问题演示

下面是一个不推荐的例子。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ExecutorsRiskDemo {

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        for (int i = 1; i <= 100000; i++) {
            int taskNo = i;

            executor.execute(() -> {
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }

                System.out.println("执行任务:" + taskNo);
            });
        }

        executor.shutdown();
    }
}

这个例子的问题是:

任务提交非常快
线程只有 2 个
任务执行很慢
队列会不断堆积

如果任务量非常大,就可能导致内存压力。

这就是为什么不建议无脑使用 Executors.newFixedThreadPool()


十六、实际开发中线程池怎么配置?

线程池参数没有万能公式。

一般要根据任务类型来配置。

1. CPU 密集型任务

比如:

  • 复杂计算;
  • 加密解密;
  • 大量对象转换;
  • 本地计算。

这类任务主要消耗 CPU。

线程数不宜太多。

一般可以参考:

CPU 核心数 + 1

示例:

int cpuCount = Runtime.getRuntime().availableProcessors();

ThreadPoolExecutor executor = new ThreadPoolExecutor(
        cpuCount + 1,
        cpuCount + 1,
        60,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(500),
        new NamedThreadFactory("cpu-task-pool"),
        new ThreadPoolExecutor.CallerRunsPolicy()
);

2. IO 密集型任务

比如:

  • 查数据库;
  • 调远程接口;
  • 读写文件;
  • 网络请求。

这类任务经常在等待 IO 返回。

线程数可以比 CPU 核心数多一些。

比如:

CPU 核心数 * 2

或者根据接口耗时、QPS、机器资源压测后调整。

示例:

int cpuCount = Runtime.getRuntime().availableProcessors();

ThreadPoolExecutor executor = new ThreadPoolExecutor(
        cpuCount * 2,
        cpuCount * 4,
        60,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(1000),
        new NamedThreadFactory("io-task-pool"),
        new ThreadPoolExecutor.CallerRunsPolicy()
);

注意:

这只是参考,不是绝对标准。

真实项目里还是要看任务耗时、数据量、数据库连接池、下游接口承载能力。


十七、实际案例:批量任务使用线程池

比如有一批用户数据,需要调用远程接口处理。

不要直接在循环里串行处理:

for (Long userId : userIds) {
    processUser(userId);
}

可以用线程池并发处理。

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class BatchTaskDemo {

    public static void main(String[] args) throws InterruptedException {
        List<Long> userIds = Arrays.asList(1001L, 1002L, 1003L, 1004L, 1005L);

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                3,
                6,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(100),
                new NamedThreadFactory("batch-user-pool"),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        CountDownLatch latch = new CountDownLatch(userIds.size());

        for (Long userId : userIds) {
            executor.execute(() -> {
                try {
                    processUser(userId);
                } finally {
                    latch.countDown();
                }
            });
        }

        latch.await();
        executor.shutdown();

        System.out.println("批量任务处理完成");
    }

    private static void processUser(Long userId) {
        System.out.println(Thread.currentThread().getName() + " 处理用户:" + userId);

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    static class NamedThreadFactory implements ThreadFactory {

        private final String prefix;
        private final AtomicInteger index = new AtomicInteger(1);

        public NamedThreadFactory(String prefix) {
            this.prefix = prefix;
        }

        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            thread.setName(prefix + "-" + index.getAndIncrement());
            return thread;
        }
    }
}

这里用了:

  • 自定义线程池;
  • 自定义线程名称;
  • 有界队列;
  • CallerRunsPolicy
  • CountDownLatch 等待所有任务完成。

这种写法在批量任务里比较常见。


十八、线程池使用注意事项

1. 不要无脑创建很多线程池

线程池不是越多越好。

一个系统里如果到处 new 线程池,很容易导致线程数不可控。

建议按业务类型统一管理。


2. 队列不要无界

无界队列容易堆积任务。

任务量一大,可能导致内存风险。

建议使用有界队列。


3. 线程要命名

线程名称很重要。

线上排查问题时,通过线程名可以快速定位是哪个业务线程池。


4. 拒绝策略要根据业务选择

如果任务不能丢,别用 DiscardPolicy

如果允许调用线程自己执行,可以考虑 CallerRunsPolicy

如果任务非常重要,要记录日志或落库补偿。


5. 记得关闭线程池

如果线程池不再使用,需要关闭。

executor.shutdown();

如果需要强制关闭:

executor.shutdownNow();

一般优先使用 shutdown()


十九、面试回答模板

如果面试官问:

线程池 7 个参数是什么?执行流程是什么?

可以这样回答:

线程池主要用来复用线程、控制并发数量,避免频繁创建线程带来的性能损耗,也避免线程数量失控。

ThreadPoolExecutor 有 7 个核心参数:
corePoolSize 是核心线程数;
maximumPoolSize 是最大线程数;
keepAliveTime 和 unit 表示非核心线程空闲多久后回收;
workQueue 是任务队列;
threadFactory 用来创建线程,通常会自定义线程名;
handler 是拒绝策略。

线程池执行任务时,先看核心线程数是否已满。
如果核心线程没满,就创建核心线程执行任务;
如果核心线程满了,就把任务放入队列;
如果队列也满了,并且线程数还没到 maximumPoolSize,就创建非核心线程;
如果线程数已经达到 maximumPoolSize,队列也满了,就触发拒绝策略。

实际开发中不建议直接使用 Executors,因为很多默认队列是无界的,可能导致任务堆积和 OOM。更推荐手动创建 ThreadPoolExecutor,明确线程数、队列长度、线程名称和拒绝策略。

这段回答基本够用了。


二十、一句话总结

线程池不是只背 7 个参数。

更重要的是理解:

任务提交后,线程池到底怎么处理?

核心流程是:

核心线程
    ↓
任务队列
    ↓
非核心线程
    ↓
拒绝策略

面试时只要把下面几点讲清楚:

  • 线程池为什么存在;
  • 7 个参数分别干什么;
  • 任务执行流程;
  • 常见队列和拒绝策略;
  • 为什么不建议使用 Executors;
  • 实际开发怎么配置;

基本就不会乱。

大家加油:)


Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐