引言
在Java并发编程中,线程池是一种被广泛应用的资源管理工具,它能够有效地管理和控制程序中的线程数量,从而提高系统性能、减少系统资源消耗。本文将深入剖析Java线程池的工作机制与执行过程。
一、线程池的基本概念
Java中的线程池主要由java.util.concurrent ThreadPoolExecutor
类实现,其核心组件包括:工作队列(如ArrayBlockingQueue)、线程池(包含核心线程和非核心线程)以及拒绝策略等。线程池的主要任务是接收外部提交的任务并调度线程进行执行。
二、线程池的工作流程
直接上图:
线程池执行过程主要分为四个阶段
任务提交阶段
当我们调用ThreadPoolExecutor.execute(Runnable task)
方法向线程池提交一个任务时,线程池内部会经历以下步骤:
- 判断当前线程池中的线程数量是否小于核心线程数:如果是,线程池会创建一个新的工作线程来执行这个任务,并将其初始化,然后调用线程的start()方法启动它。
- 若当前线程数不小于核心线程数,线程池会尝试将任务添加到工作队列(如ArrayBlockingQueue)。这里的“尝试”意味着如果队列已满,那么就会触发下一种处理方式。
- 如果工作队列已满,线程池会检查当前线程数是否小于最大线程数。如果小于最大线程数,线程池会创建一个新的线程来处理这个任务;否则,就需要采取拒绝策略来处理无法接受的新任务。
任务获取与执行阶段
每个工作线程都在一个无限循环中运行,它们会不断从工作队列中获取任务并执行。获取任务的过程通常涉及阻塞等待(如使用BlockingQueue的take()方法)。一旦获取到任务,线程就会执行Runnable.run()
方法,完成任务的实际工作。
线程扩展与收缩
- 线程扩展:当线程池需要创建新线程来处理任务时,会调用
ThreadFactory.newThread(Runnable r)
方法创建新线程。默认的线程工厂会为每个线程赋予有意义的名字,比如"pool-1-thread-1"。 - 线程收缩:线程池中的非核心线程在完成任务后不会立即销毁,而是进入保持存活状态。只有当这些线程在指定时间内(keepAliveTime参数指定)没有接收到新的任务,并且线程池中的线程数大于核心线程数时,这些空闲线程才会被终止。
拒绝策略
四种常见的拒绝策略如下:
- AbortPolicy:默认拒绝策略,直接抛出RejectedExecutionException异常。
- CallerRunsPolicy:调用者所在线程负责执行任务,这将降低新任务的提交速度。
- DiscardPolicy:默默地丢弃任务,不执行也不抛出异常。
- DiscardOldestPolicy:移除工作队列中最旧的任务(最先入队但尚未被执行的任务),然后重新尝试提交当前任务。
线程池核心参数
Java线程池的核心类是java.util.concurrent.ThreadPoolExecutor
,它提供了丰富的参数用于定制线程池的行为。以下是ThreadPoolExecutor的主要构造函数参数及其含义:
- corePoolSize: 这是线程池的基本大小,即线程池即使在空闲时也会维持的最小线程数量。只要有任务提交过来,就会优先创建至corePoolSize个线程来处理任务。
- maximumPoolSize: 这是线程池能容纳的最大线程数。当线程池中的所有线程都处于活动状态且工作队列已满时,线程池会尝试增加更多的线程来处理任务,直到达到这个上限。
- keepAliveTime: 当线程池中线程数量超过corePoolSize时,多余的空闲线程在多长时间内(无新任务提交)仍不被使用就会被终止。这个时间长度是以TimeUnit指定的时间单位来衡量的。
- TimeUnit: keepAliveTime参数的时间单位,可以是纳秒(NANOSECONDS)、微秒(MICROSECONDS)、毫秒(MILLISECONDS)、秒(SECONDS)、分钟(MINUTES)、小时(HOURS)或天(DAYS)。
- BlockingQueue workQueue: 任务队列,用于存储等待执行的任务。可以选择不同的队列类型,例如无界队列(如LinkedBlockingQueue)、有界队列(如ArrayBlockingQueue)或其他满足BlockingQueue接口的自定义队列。
- ThreadFactory threadFactory: 用于创建新线程的工厂类,可以通过自定义ThreadFactory来设定新线程的名称、优先级和其他属性。
- RejectedExecutionHandler handler: 拒绝策略,当线程池和任务队列都无法处理新的任务时,会调用这个策略的rejectedExecution方法来处理被拒绝的任务。Java提供了一系列内置的拒绝策略如AbortPolicy、CallerRunsPolicy、DiscardPolicy和DiscardOldestPolicy,也可以自定义拒绝策略。
线程池实践
创建一个核心线程池数为2,最大线程数为3,阻塞队列大小为2的线程池。
下面的代码中,执行一次任务需要5000MS,而6个任务是一次性提交进去的,其中第四个任务就会因为 无法被核心线程执行,无法加入等待队列,无法创建新的非核心线程执行,而执行拒绝策略。
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executorService = new ThreadPoolExecutor(2,
3,
0, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(2));
for (int i = 0; i < 6; i++) {
executorService.execute(() -> {
try {
Date date = new Date();
System.out.println("线程:" + Thread.currentThread().getName() + "报时:" + date);
Thread.sleep(5000);
} catch (InterruptedException e) {
}
});
System.out.println("等待队列中现在有" + executorService.getQueue().size() + "个任务");
Thread.sleep(500);
}
}
运行结果如下:
从运行结果中,可以看出
1、前面两个任务是通过核心线程池来执行
2、第3、4个任务会被放到等待队列中
3、等待队列满了后,第五个任务是通过创建了一个非核心线程来执行任务,这是第三个线程
4、第六个任务由于核心线程池,等待队列,最大线程池数都已经达到最大,所以执行拒绝策略
5、从队列中获取任务,由于核心线程里面的任务已经执行完成,所以通过核心线程执行