Java 的线程池工作过程
本文由 简悦 SimpRead 转码, 原文地址 www.bilibili.com
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?
在 Java 中可以通过线程池来达到这样的效果。今天我们就来详细讲解一下 Java 的线程池,首先我们从最核心的 ThreadPoolExecutor 类中的方法讲起,然后再讲述它的实现原理,接着给出了它的使用示例,最后讨论了一下如何合理配置线程池的大小。
java.uitl.concurrent.ThreadPoolExecutor 类是线程池中最核心的一个类,通过查看源码, 我们可以知道这个类继承了 AbstractExecutorService 抽象类.
下面我们来看一下 ThreadPoolExecutor 类中的构造方法:
-
ThreadPoolExecutor 类中共有 4 个构造方法, 前三个构造方法其实最终调用的都是最后一个构造方法, 也就是下面这个参数最全的构造方法.
-
构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler){......}
接下来我们来看一下这个构造器中, 每个参数的含义分别是什么:
-
corePoolSize
-
核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了 prestartAllCoreThreads() 或者 prestartCoreThread() 方法,从这 2 个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建 corePoolSize 个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为 0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到 corePoolSize 后,就会把到达的任务放到缓存队列当中;
-
maximumPoolSize
-
线程池中最大线程数, 用来表示线程池中最多能创建多少个线程.
-
keepAliveTime
-
线程的存活时间, 表示线程没有任务执行时最多保持多久时间会终止。
-
默认情况下,只有当线程池中的线程数大于 corePoolSize 时,keepAliveTime 才会起作用,直到线程池中的线程数不大于 corePoolSize,即当线程池中的线程数大于 corePoolSize 时,如果一个线程空闲的时间达到 keepAliveTime,则会终止,直到线程池中的线程数不超过 corePoolSize。但是如果调用了 allowCoreThreadTimeOut(boolean) 方法,在线程池中的线程数不大于 corePoolSize 时,keepAliveTime 参数也会起作用,直到线程池中的线程数为 0;
-
unit
-
TimeUnit.DAYS; // 天
-
TimeUnit.HOURS; // 小时
-
TimeUnit.MINUTES; // 分钟
-
TimeUnit.SECONDS; // 秒
-
TimeUnit.MILLISECONDS; // 毫秒
-
TimeUnit.MICROSECONDS; // 微妙
-
TimeUnit.NANOSECONDS; // 纳秒
-
参数 keepAliveTime 的时间单位,有 7 种取值,在 TimeUnit 类中有 7 种静态属性:
-
workQueue
-
ArrayBlockingQueue;
-
一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:
-
LinkedBlockingQueue;
-
SynchronousQueue;
-
threadFactory
-
线程工厂,用来创建线程,主要是为了给线程起名字,默认工厂的线程名字 pool-1-thread-3。
-
Handler
-
ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出 RejectedExecutionException 异常。
-
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
-
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
-
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
-
拒绝策略,当线程池里线程被耗尽,且队列也满了的时候会调用。
以上就是创建线程池时用到的几个参数,面试中经常会有面试官问到这些参数的含义.
下图为线程池的执行流程
-
如果当前工作线程数小于核心线程,则创建核心线程执行任务。
-
如果当前线程大于核心线程数则判断等待队列是否已满,如果没有满则添加任务到等待队列中去,如果工作线程数量为 0 则创建非核心线程,并从等待队列中拉取任务执行。
-
最后如果队列已满创建一个非核心线程执行任务。如果创建失败则会拒绝任务。
ThreadPoolExecutor 是线程池的实现类, 无论是自定义线程池, 还是使用系统提供的线程池, 都会使用到这个类. 通过类的 execute(Runnable command) 方法来执行 Runnable 任务.
-
判断当前活跃线程数是否小于 corePoolSize, 如果小于,则调用 addWorker 创建线程执行任务
-
如果不小于 corePoolSize,则将任务添加到 workQueue 队列。
-
如果放入 workQueue 失败,则创建线程执行任务,如果这时创建线程失败 (当前线程数不小于 maximumPoolSize 时),就会调用 reject(内部调用 handler) 拒绝接受任务。
在 execute() 方法中获知通过 addWorker() 方法来添加新线程, 那么到底是如何添加和管理的?
这块代码是在创建非核心线程时,即 core 等于 false。判断当前线程数是否大于等于 maximumPoolSize,如果大于等于则返回 false,即上边说到的 3 中创建线程失败的情况。
创建 worker 对象, 并将 Runnable 作为参数传递进去, 并从 worker 中取出 Thread 对象, 进行一系列条件判断后.
开启 Thread 的 start() 方法, 线程开始运行. 所以 worker 对象中必然包含了一个 Thread 和一个要被执行的 Runnable.
-
每个 worker, 都是一条线程, 同时里面包含了一个 firstTask, 即初始化时要被首先执行的任
-
最终执行任务的, 是 runWorker() 方法
线程调用 runWoker,会 while 循环调用 getTask 方法从 workerQueue 里读取任务,然后执行任务。只要 getTask 方法不返回 null, 此线程就不会退出。
-
先不管 allowCoreThreadTimeOut,这个变量默认值是 false。wc>corePoolSize 则是判断当前线程数是否大于 corePoolSize。
-
如果当前线程数大于 corePoolSize,则会调用 workQueue 的 poll 方法获取任务,超时时间是 keepAliveTime。如果超过 keepAliveTime 时长,poll 返回了 null,上边提到的 while 循序就会退出,线程也就执行完了。
最近收集整理一份面试资料,覆盖了 Java 核心技术、JVM、Java 并发、SSM、微服务、数据库、数据结构等等技术点, 有兴趣的同学可以 + VX: congqing24 获取相关资料.