线程池

简介

线程池本质上就是一组线程的集合,当使用线程时会从集合中取出一个线程,当时用完后,该线程会重新回到集合中

image-20240602190203776

参数

Java中创建线程池:

1
new ThreadPoolExecutor(10, 200, 1000, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(200))
  1. corePoolSize(核心线程数):

    这是线程池中会始终保持活动的线程数,即使它们目前是空闲的。这些线程不会因为执行完任务后处于空闲状态而被终止,除非设置了allowCoreThreadTimeOuttrue

  2. maximumPoolSize(最大线程数):

    这是线程池能够容纳同时执行的最大线程数。当活动线程数达到这个值后,新来的任务将会被阻塞或拒绝,具体行为取决于所使用的阻塞队列和拒绝策略。

  3. keepAliveTime(空闲线程存活时间):

    如果线程池中的线程数量超过corePoolSize,那么多余的空闲线程在等待新任务到达的最长时间。超过这个时间后,多余的空闲线程会被终止。这个参数对核心线程不生效,除非设置了allowCoreThreadTimeOuttrue

  4. unit(空闲线程存活时间单位):

    用来指定keepAliveTime参数的时间单位,如TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)等。

  5. workQueue(工作队列/任务队列):

    当新任务到来时,如果线程池中的线程数量已经达到了maximumPoolSize,那么新任务会被放入这个队列中等待执行。根据不同的队列类型(如LinkedBlockingQueueArrayBlockingQueueSynchronousQueue等),线程池的行为会有很大不同。

除了以上的五重必备的参数,还有两种可选的参数

  1. threadFactory(线程工厂):

    用于创建新线程的工厂,可以用来设置新线程的优先级、名称、是否为守护线程等属性。

  2. rejectedExecutionHandler(拒绝策略):

    当线程池和工作队列都满时,对于新提交的任务所采取的策略,比如AbortPolicy(抛出异常)、CallerRunsPolicy(调用者运行)、DiscardPolicy(静默丢弃)或DiscardOldestPolicy(丢弃队列中最旧的任务并尝试重新提交当前任务)

核心线程不会再创建线程池的一瞬间全部创建完成,直接常见的线程池是空的。只有当任务使用线程持的时候才会创建一个线程,直到数量等于核心线程数(哪怕之前的线程已经完成任务也不会使用之前的线程而是继续创建)。

如果想要再线程池创建的时候预创建线程可以使用以下两个方法

1
2
3
4
// 启动一个核心线程(前提是线程池中的线程数量没有达到最大核心线程数)
pool.prestartCoreThread();
// 启动所有核心线程
pool.prestartAllCoreThreads();

核心线程数

对于CPU密集型任务:CPU核心数 + 1

IO密集型任务(文件IO、网络IO):

  • 公式一:CPU核心数 * 2
  • 公式二:CPU核心数 * (1 + 线程等待时间 / 线程运行时间)
    • 线程等待时间:线程没有使用CPU的时间,如:阻塞IO
    • 线程运行时间:线程执行完某个任务的总时间
1
2
// 获取CPU内核数量
int numberOfCores = Runtime.getRuntime().availableProcessors();

执行任务具体流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public void execute(Runnable command) {
// 如果传过来的任务是空的,则报空指针异常
if (command == null)
throw new NullPointerException();

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);
}

image-20240602200640548

五种状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
原文:
RUNNING: Accept new tasks and process queued tasks
SHUTDOWN: Don't accept new tasks, but process queued tasks
STOP: Don't accept new tasks, don't process queued tasks,
and interrupt in-progress tasks
TIDYING: All tasks have terminated, workerCount is zero,
the thread transitioning to state TIDYING
will run the terminated() hook method
TERMINATED: terminated() has completed

翻译:
RUNNING:接受新任务并处理排队任务
SHUTDOWN:不接受新任务,但处理排队任务
STOP:不接受新任务,不处理排队任务,中断正在进行的任务
TIDYING:所有任务都已终止,workerCount为零,线程转换到 TIDYING 状态,会运行terminate()钩子方法
TERMINATED:terminated()已完成

通过调用以下任意方法进入状态

1
2
pool.shutdown(); // SHUTDOWN
pool.shutdownNow(); // STOP

以上任意方法执行完成之后线程池中的所有线程都会被关闭(队列不一定清空),然后进入 TIDYING 状态。当进入到 TIDYING 状态后会调用 terminate() 方法(这是一个钩子函数,可以被重写一些逻辑),然后进入 TERMINATED 状态

当执行两种方法时是先设置状态然后再依照对应的规则关闭线程,为的就是防止后续任务的提交