Skip to content

线程池面试问题总结 ​

看在前面 ​

  • ThreadPoolExecutor.md
  • [JavaConcurrencyAdvancedCommonInterviewQuestions.md]((原链已失效 2026-10,内容已在本篇/本库)s.md#41-%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E7%94%A8%E7%BA%BF%E7%A8%8B%E6%B1%A0)
  • [java线程池学习总结(Executor 框架,原《Executor 框架》篇并入本篇)]((原链已失效 2026-10,内容已在本篇/本库)%A0%E6%80%BB%E7%BB%93.md#%E4%BA%8C-executor-%E6%A1%86%E6%9E%B6)

Executor 框架结构 ​

整理自 [java线程池学习总结]((原链已失效 2026-10,内容已在本篇/本库)%A0%E6%80%BB%E7%BB%93.md#%E4%BA%8C-executor-%E6%A1%86%E6%9E%B6)。

Executor 框架是 Java 5 之后引进的。通过 Executor 来启动线程比使用 Thread 的 start 方法更好。除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题。

补充:this 逃逸是指在构造函数返回之前其他线程就持有该对象的引用。调用尚未构造完全的对象的方法可能引发令人疑惑的错误。

Executor 框架不仅包括了线程池的管理,还提供了线程工厂、队列、拒绝策略这类组件。Executor 框架让并发编程变得更加简单。

Executor 框架结构主要由三大部分组成:

1. 任务(Runnable /Callable)

执行任务需要实现的 Runnable 接口 或 Callable接口。Runnable 接口或 Callable 接口 实现类都可以被 ThreadPoolExecutor 或 ScheduledThreadPoolExecutor 执行。

2. 任务的执行(Executor)

包括任务执行机制的核心接口 Executor ,以及继承自 Executor 接口的 ExecutorService 接口。ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 这两个关键类实现了 ExecutorService 接口。

这里提了很多底层的类关系。但是,我们需要更多关注的是 ThreadPoolExecutor 这个类。这个类在我们实际使用线程池的过程中,使用频率高。

注意: 通过查看 ScheduledThreadPoolExecutor 源代码我们发现 ScheduledThreadPoolExecutor 继承了 ThreadPoolExecutor 并实现了 ScheduledExecutorService。而 ScheduledExecutorService 又实现了 ExecutorService。

ThreadPoolExecutor 类描述:

java
//AbstractExecutorService实现了ExecutorService接口
public class ThreadPoolExecutor extends AbstractExecutorService

ScheduledThreadPoolExecutor 类描述:

java
//ScheduledExecutorService实现了ExecutorService接口
public class ScheduledThreadPoolExecutor
        extends ThreadPoolExecutor
        implements ScheduledExecutorService

3. 异步计算的结果(Future)

Future 接口以及 Future 接口的实现类 FutureTask 类都可以代表异步计算的结果。

当我们把 Runnable接口 或 Callable 接口 的实现类提交给 ThreadPoolExecutor 或 ScheduledThreadPoolExecutor 执行时,调用 submit() 方法会返回一个 FutureTask 对象。

Executor 框架的使用示意图 ​

Executor框架使用示意图

  1. 主线程创建实现了 Runnable 或 Callable 接口的任务对象;
  2. 把任务对象交给 ExecutorService 执行:execute(Runnable command),或 submit(Runnable task)、submit(Callable<T> task);
  3. 执行 submit(...) 时,ExecutorService 返回一个实现 Future 接口的对象。FutureTask 实现了 Runnable,也可以直接创建 FutureTask 交给 ExecutorService 执行;
  4. 主线程执行 FutureTask.get() 等待任务完成,或执行 FutureTask.cancel(mayInterruptIfRunning) 取消任务。

Question ​

  • 为什么要用线程池?
  • Runnable vs Callable
  • execute() vs submit()
  • shutdown() vs shutdownNow()
  • isTerminated() vs isShutdown()
  • 如何创建线程池
  • ThreadPoolExecutor构造函数重要参数分析
  • ThreadPoolExecutor中execute(Runnable command)方法流程分析
  • 线程池是如何复用线程的呢?
  • 当线程池中线程出现异常时,线程池对该线程操作是什么呢?
  • 线程池是如何区分核心线程和非核心线程的?
  • 线程池是如何判断该回收非核心线程的?
  • ThreadPoolExecutor 四种饱和策略

为什么要用线程池? ​

池化技术想必大家已经屡见不鲜了。线程池、数据库连接池、Http 连接池这类技术都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。

线程池提供了一种限制和管理资源(包括执行一个任务)。每个线程池还维护一些基本统计信息,例如已完成任务的数量。

这里借用《Java 并发编程的艺术》提到的来说明使用线程池的好处:

  • 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。 当任务到达时,任务可以不用等到线程创建就能立即执行。
  • 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性。使用线程池可以统一分配、调优和监控。

实现Runnable接口和Callable接口的区别 ​

Runnable自Java 1.0以来一直存在,但Callable仅在Java 1.5中引入,目的就是为了来处理Runnable不支持的用例。Runnable 接口不会返回结果或抛出检查异常,但是 Callable 接口 可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。

工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。(Executors.callable(Runnable task)或 Executors.callable(Runnable task,Object resule))。关于callable方法可以参照JDK源码实现了解Runnable和Callable对象转换过程。

执行execute()方法和submit()方法的区别是什么呢? ​

1 execute()方法用于提交不需要返回值的任务。这种方式无法判断任务是否被线程池执行成功与否; 2 **submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象。**通过这个 Future 对象可以判断任务是否执行成功,并可以通过 Future 的 get()方法来获取返回值。get()方法会阻塞当前线程直到任务完成。而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

我们以 AbstractExecutorService 接口中的一个 submit 方法为例子来看看源代码:

java
public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}

上面方法调用的 newTaskFor 方法返回了一个 FutureTask 对象。

java
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
    return new FutureTask<T>(runnable, value);
}

我们再来看看execute()方法:

java
public void execute(Runnable command) {
  ...
}

shutdown() vs shutdownNow() ​

  • shutdown():关闭线程池,线程池的状态变为SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。

  • shutdownNow():关闭线程池,线程的状态变为STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。

isTerminated() vs isShutdown() ​

  • isShutdown():当调用 shutdown() 方法后返回为 true。

  • isTerminated():当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true。

如何创建线程池 ​

《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式。这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

Executors 返回线程池对象的弊端如下:

FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致OOM。

CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。

方式一:通过构造方法实现

ThreadPoolExecutor构造方法

方式二:通过Executor 框架的工具类Executors来实现,我们可以创建三种类型的ThreadPoolExecutor:

  • FixedThreadPool:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。

  • SingleThreadExecutor:方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中。待线程空闲,按先入先出的顺序执行队列中的任务。

  • CachedThreadPool:该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池复用。

对应Executors工具类中的方法如图所示:

Executor框架的工具类

ThreadPoolExecutor 类分析 ​

ThreadPoolExecutor类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么),这里就不贴代码讲了,比较简单。

java
/**
 * 用给定的初始参数创建一个新的ThreadPoolExecutor。
 */
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

下面这些内容在后面使用线程池的过程中一定会用到,创建时同样重要。所以,必须拿着小本本记清楚。

ThreadPoolExecutor构造函数重要参数分析 ​

ThreadPoolExecutor 3 个最重要的参数:

  • corePoolSize : 核心线程数定义了最小可以同时运行的线程数量。
  • maximumPoolSize:当队列中存放的任务达到队列容量时,当前可以同时运行的线程数量变为最大线程数。
  • workQueue: 当新任务来的时候,会先判断当前运行的线程数量是否达到核心线程数。如果达到,新任务就会被存放在队列中。

ThreadPoolExecutor其他常见参数:

  • keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交。核心线程外的线程不会立即销毁,而是会等待。直到等待的时间超过了 keepAliveTime,才会被回收销毁;
  • unit : keepAliveTime 参数的时间单位。
  • threadFactory :executor 创建新线程的时候会用到。
  • handler :饱和策略。关于饱和策略下面单独说明。

下面这张图可以加深你对线程池中各个参数的相互关系的理解(图片来源:《Java性能调优实战》):

ThreadPoolExecutor构造方法各个参数的关系

一个简单的线程池Demo:Runnable+ThreadPoolExecutor ​

首先创建一个 Runnable 接口的实现类(当然也可以是 Callable 接口,我们上面也说了两者的区别。)

ThreadPoolExecutorDemo.java

java
import java.util.Date;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutorDemo {

	private static final int CORE_POOL_SIZE = 5;

	private static final int MAX_POOL_SIZE = 10;

	private static final int QUEUE_CAPACITY = 100;

	private static final Long KEEP_ALIVE_TIME = 1L;

	public static void main(String[] args) {
		ThreadPoolExecutor executor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS,
				new ArrayBlockingQueue<>(QUEUE_CAPACITY), new ThreadPoolExecutor.CallerRunsPolicy());

		for (int i = 0; i < 10; i++) {
			Runnable worker = new MyRunnable("" + i);
			executor.execute(worker);
		}

		executor.shutdown();
		while (!executor.isTerminated()) {
		}
		System.out.println("Finished all threads");
	}

	public static class MyRunnable implements Runnable {

		private String command;

		public MyRunnable(String command) {
			super();
			this.command = command;
		}

		@Override
		public void run() {
			System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
			processCommand();
			System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
		}

		private void processCommand() {
			try {
				Thread.sleep(5000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}

		@Override
		public String toString() {
			return this.command;
		}

	}

}

可以看到我们上面的代码指定了:

  1. corePoolSize:核心线程数为 5。
  2. maximumPoolSize:最大线程数 10
  3. keepAliveTime:等待时间为 1L。
  4. unit:等待时间的单位为 TimeUnit.SECONDS。
  5. workQueue:任务队列为 ArrayBlockingQueue,容量为 100;
  6. handler:饱和策略为 CallerRunsPolicy。

Output:

java
pool-1-thread-2 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-5 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-4 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-1 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-3 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-5 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-3 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-4 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-1 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-1 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-4 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-3 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-5 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-3 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-4 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-5 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019

线程池原理分析 ​

我们通过代码输出结果可以看出:线程池每次会同时执行 5 个任务。这 5 个任务执行完之后,剩余的 5 个任务才会被执行。

现在,我们就分析上面的输出内容来简单分析线程池原理。

为了搞懂线程池的原理,我们需要首先分析 execute方法。 上面的Demo 中我们使用 executor.execute(worker)来提交一个任务到线程池中去。这个方法非常重要,下面我们来看看它的源码:

java
// 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount)
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
java
/**
 * Executes the given task sometime in the future.  The task
 * may execute in a new thread or in an existing pooled thread.
 *
 * If the task cannot be submitted for execution, either because this
 * executor has been shutdown or because its capacity has been reached,
 * the task is handled by the current {@code RejectedExecutionHandler}.
 *
 * @param command the task to execute
 * @throws RejectedExecutionException at discretion of
 *         {@code RejectedExecutionHandler}, if the task
 *         cannot be accepted for execution
 * @throws NullPointerException if {@code command} is null
 */
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    /*
     * Proceed in 3 steps:
     *
     * 1. If fewer than corePoolSize threads are running, try to
     * start a new thread with the given command as its first
     * task.  The call to addWorker atomically checks runState and
     * workerCount, and so prevents false alarms that would add
     * threads when it shouldn't, by returning false.
     *
     * 2. If a task can be successfully queued, then we still need
     * to double-check whether we should have added a thread
     * (because existing ones died since last checking) or that
     * the pool shut down since entry into this method. So we
     * recheck state and if necessary roll back the enqueuing if
     * stopped, or start a new thread if there are none.
     *
     * 3. If we cannot queue task, then we try to add a new
     * thread.  If it fails, we know we are shut down or saturated
     * and so reject the task.
     */
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}

三步走的中文版:

  1. 线程数 < corePoolSize:新建核心线程执行任务;
  2. 线程数 ≥ corePoolSize:任务入队;
  3. 队列满:继续开新线程直到 maximumPoolSize。若线程数已达 maximumPoolSize 且队列也满,触发拒绝策略。

**核心线程看线程数,队列看是否还有空位,最大线程看队列满不满,拒绝策略看两者是否都到顶。**这就是四个参数各自的生效时机。

线程池execute流程图如下:

ThreadPoolExecutor中execute(Runnable command)方法流程

针对上面写的 Demo, 现在应该是不是很容易就可以搞懂它的原理了呢?没搞懂也没关系,可以看看下面的分析:

我们在代码中模拟了 10 个任务:核心线程数为 5、等待队列容量为 100。所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。

ThreadPoolExecutor 四种饱和策略 ​

ThreadPoolExecutor 饱和策略定义:

如果当前同时运行的线程数量达到最大线程数量,并且队列也已经被放满。此时,ThreadPoolExecutor 定义一些策略:

  • ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,不会丢弃任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟,并且你不能丢弃任何一个任务请求。那么你可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。

其中关于CallerRunsPolicy策略,可以结合JDK中ThreadPoolExecutor.CallerRunsPolicy源码:

java
/**
 * A handler for rejected tasks that runs the rejected task
 * directly in the calling thread of the {@code execute} method,
 * unless the executor has been shut down, in which case the task
 * is discarded.
 */
public static class CallerRunsPolicy implements RejectedExecutionHandler {
    /**
     * Creates a {@code CallerRunsPolicy}.
     */
    public CallerRunsPolicy() { }

    /**
     * Executes task r in the caller's thread, unless the executor
     * has been shut down, in which case the task is discarded.
     *
     * @param r the runnable task requested to be executed
     * @param e the executor attempting to execute the task
     */
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            r.run();
        }
    }
}

举个例子: Spring 通过 ThreadPoolTaskExecutor,或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候。如果不指定 RejectedExecutionHandler 饱和策略来配置线程池,默认使用的是 ThreadPoolExecutor.AbortPolicy。在默认情况下,ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务。这代表你将丢失对这个任务的处理。对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 ThreadPoolExecutor 的构造函数源码就可以看出,原因简单,这里就不贴代码了)

示例代码 ​

java
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolRejectedPolicyTest {

	/** corePoolSize and maximumPoolSize size **/
	private static final int THREAD_SIZE = 2;
	/** ThreadPoolExecutor.AbortPolicy Saturation strategy **/
	private static final int ABORT_POLICY = 0;
	/** ThreadPoolExecutor.CallerRunsPolicy Saturation strategy **/
	private static final int CALLER_RUNS_POLICY = 1;
	/** ThreadPoolExecutor.DiscardPolicy Saturation strategy **/
	private static final int DISCARD_POLICY = 2;
	/** ThreadPoolExecutor.DiscardOldestPolicy Saturation strategy **/
	private static final int DISCARD_OLDEST_POLICY = 3;

	/**
	 * ThreadPoolExecutor choice Saturation strategy, default strategy:AbortPolicy
	 **/
	private static int selectedPolicy = ABORT_POLICY;

	public static void main(String[] args) {
		ThreadPoolExecutor executor = new ThreadPoolExecutor(THREAD_SIZE, THREAD_SIZE, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(1),
				getPolicy(selectedPolicy));
		for (int i = 0; i < 5; i++) {
			executor.submit(new Task(i));
		}

		executor.shutdown();
		while (!executor.isTerminated()) {
		}
		System.out.println("Finished all threads");
	}

	public static RejectedExecutionHandler getPolicy(int policy) {
		switch (policy) {
		case ABORT_POLICY:
			return new ThreadPoolExecutor.AbortPolicy();
		case CALLER_RUNS_POLICY:
			return new ThreadPoolExecutor.CallerRunsPolicy();
		case DISCARD_POLICY:
			return new ThreadPoolExecutor.DiscardPolicy();
		case DISCARD_OLDEST_POLICY:
			return new ThreadPoolExecutor.DiscardOldestPolicy();
		default:
			break;
		}

		return new ThreadPoolExecutor.AbortPolicy();
	}

	public static class Task implements Runnable {

		private int taskId;

		public Task(int id) {
			taskId = id;
		}

		@Override
		public void run() {
			try {
				Thread.sleep(50);
			} catch (InterruptedException e) {
				e.getStackTrace();
			}

			System.out.println("Task " + taskId + " is running on " + Thread.currentThread().getName());
		}
	}

}

AbortPolicy ​

将上面代码的变量 selectedPolicy 设置为 ABORT_POLICY。

java
selectedPolicy = ABORT_POLICY

Output:

java
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@75b84c92 rejected from java.util.concurrent.ThreadPoolExecutor@6bc7c054[Running, pool size = 2, active threads = 2, queued tasks = 1, completed tasks = 0]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(Unknown Source)
	at java.util.concurrent.ThreadPoolExecutor.reject(Unknown Source)
	at java.util.concurrent.ThreadPoolExecutor.execute(Unknown Source)
	at java.util.concurrent.AbstractExecutorService.submit(Unknown Source)
	at com.demotransfer.java.concurrent.ThreadPoolRejectedPolicyTest.main(ThreadPoolRejectedPolicyTest.java:30)
Task 1 is running on pool-1-thread-2
Task 0 is running on pool-1-thread-1
Task 2 is running on pool-1-thread-1

如上所示,执行了三个任务。第四个提交的时候,直接抛出异常,JVM 一直处于运行状态。任务0和任务1一提交就被线程池中的两个线程执行,任务2提交到阻塞队列等待执行。此时继续提交任务,由于队列已满,触发饱和策略,直接抛出异常。

CallerRunsPolicy ​

将上面代码的变量 selectedPolicy 设置为 CALLER_RUNS_POLICY。

java
selectedPolicy = CALLER_RUNS_POLICY

Output:

java
Task 3 is running on main
Task 0 is running on pool-1-thread-1
Task 1 is running on pool-1-thread-2
Task 4 is running on main
Task 2 is running on pool-1-thread-1
Finished all threads

如上所示,所有任务都被执行,不会被抛弃也不会有异常抛出。任务0,1,2都在线程池中的线程执行。任务3,4则是由main线程执行。这说明,CallerRunsPolicy 饱和策略在队列已满的情况下,会把后面提交的任务给回调用者线程去执行。换句话说就是在调用exector的线程中运行新的任务。

DiscardPolicy ​

将上面代码的变量 selectedPolicy 设置为 DISCARD_POLICY。

java
selectedPolicy = DISCARD_POLICY

Output:

java
Task 0 is running on pool-1-thread-1
Task 1 is running on pool-1-thread-2
Task 2 is running on pool-1-thread-1
Finished all threads

如上所示,只执行了任务0,1,2。其他任务直接被抛弃。所以 DiscardPolicy 正如其名字,简单粗暴,队列满后新任务通通都抛弃。

DiscardOldestPolicy ​

将上面代码的变量 selectedPolicy 设置为 DISCARD_OLDEST_POLICY。

java
selectedPolicy = DISCARD_OLDEST_POLICY

Output:

java
Task 0 is running on pool-1-thread-1
Task 1 is running on pool-1-thread-2
Task 4 is running on pool-1-thread-2
Finished all threads

如上所示,只执行了任务0,1,4。其他任务2,3直接被抛弃。所以 DiscardOldestPolicy 也可以说正如其名字,简单粗暴,队列满后等待最久的任务将被新任务替换。

追问补全:四个高频深挖题 ​

2026-10 新增(复核补全)。本节回答 Question 清单中尚未展开的 4 个追问。

线程池是如何复用线程的呢? ​

Worker 线程执行完任务后不退出。它在 runWorker() 的循环里反复调用 getTask(),从 workQueue 取下一个任务。队列空时,核心线程阻塞在 take() 上等待新任务。非核心线程用带 keepAliveTime 超时的 poll() 取任务。线程不绑定单个任务,因此可以一直执行新任务。

当线程池中线程出现异常时,线程池对该线程操作是什么呢? ​

结果取决于任务用哪种方式提交。execute() 提交裸 Runnable:未捕获异常直接抛出,Worker 线程死亡。processWorkerExit() 随后清理并按需 addWorker() 补充新线程。submit() 的任务先包装成 FutureTask,异常在内部捕获后存入 Future。Worker 线程不会死亡。直到调用 get() 时,异常才重新抛出。

线程池是如何区分核心线程和非核心线程的? ​

线程本身没有「核心」标记。Worker 对象不保存是否核心的字段。区别只在 getTask() 的超时逻辑。工作线程数大于 corePoolSize 时,取任务改用带超时的 poll()。不超过 corePoolSize 时,无限阻塞在 take() 上。allowCoreThreadTimeOut 为 true 时,核心线程同样走带超时的 poll()。

线程池是如何判断该回收非核心线程的? ​

判断发生在 getTask() 里。非核心线程用 poll(keepAliveTime) 取任务,超时返回 null。Worker 随即退出 runWorker() 循环,进入 processWorkerExit()。该方法把 Worker 移出 workers 集合,workerCount 减一,再按条件补建线程。核心线程的 take() 不超时,不走这条回收路径。例外是 allowCoreThreadTimeOut 为 true 或 shutdown 时被中断。