若图片有问题,请点击此处查看
很久前(2020-10-23),就有想法学习线程池并输出博客,但是写着写着感觉看不懂了,就不了了之了。现在重拾起,重新写一下(学习一下)。
线程池的优点也是老生常谈的东西了
- 减少线程创建的开销(任务数大于线程数时)
- 统一管理一系列的线程(资源)
在讲ThreadPoolExecutor前,我们先看看它的父类都有些啥。
Executor,执行提交的Runnable任务的对象,将任务提交与何时执行分离开。 execute方法是Executor接口的唯一方法。
1 |
|
ExecutorService是一个Executor,提供了管理终止的方法和返回Future来跟踪异步任务的方法(sumbit)。 终止的两个方法
- shutdown(), 正在执行的任务继续执行,不接受新任务
- shutdownNow(), 正在执行的任务也要被终止
AbstractExecutorService,实现了ExecutorService的sumbit、invokeAny,invokeAll
介绍
🐱🏍线程池主要元素
底层变量
ctl
我们讲讲先ctl(The main pool control state), 其包含两个信息
- 线程池的状态(最高三位)
- 线程池的workerCount,有效的线程数
1 |
|
🐱🏍线程池的状态转化图
看一下每个状态的含义
- RUNNING, 接受新的任务并且处理阻塞队列的任务
- SHUTDOWN, 拒绝新任务,但是处理阻塞队列的任务
- STOP, 拒绝新任务,并且抛弃阻塞队列中的任务,还要中断正在运行的任务
- TIDYING,所有任务执行完(包括阻塞队列中的任务)后, 当前线程池活动线程为0, 将要调用terminated方法
- TERMINATED, 终止状态。调用terminated方法后的状态
workers
工作线程都添加到这个集合中。可以想象成一个集中管理的平台,可以通过workers获取活跃的线程数,中断所有线程等操作。
1 |
|
可修改变量
构造器中的参数
1 |
|
corePoolSize、maximumPoolSize,workQueue三者的关系:
- 当线程数小于corePoolSize,任务进入,即使有其他线程空闲,也会创建一个新的线程
- 大于corePoolSize且小于maximumPoolSize,workQueue未满,将任务加入到workQueue中;只有workQueue满了,才会新建一个线程
- 若workQueue已满,且任务大于maximumPoolSize,将会采取拒绝策略(handler)
拒绝策略:
- AbortPolicy, 直接抛出RejectedExecutionException
- CallerRunsPolicy, 使用调用者所在线程来执行任务
- DiscardPolicy, 默默丢弃
- DiscardOldestPolicy, 丢弃头部的一个任务,重试
allowCoreThreadTimeOut
控制空闲时,core threads是否被清除。
探索源码
最重要的方法就是execute
提交的任务将在未来某个时候执行
1 |
|
根据上面的注释,我们将execute分为三个部分来讲解
- 当正在运行的线程数小于corePoolSize
- 当大于corePoolSize时,需要入队
- 队列已满
当正在运行的线程数小于corePoolSize
1 |
|
addWorker, 创建工作线程。 当然它不会直接就添加一个新的工作线程,会检测runState与workCount,来避免不必要的新增。检查没问题的话,新建线程,将其加入到wokers,并将线程启动。
1 |
|
上面的代码很长,我们将它分为两部分
1 |
|
经过上面的代码,我们成功通过CAS使workerCount + 1,下面我们就会新建worker并添加到workers中,并启动通过threadFactory创建的线程。
1 |
|
看完了addWorker的步骤,代码中有个Worker类,看似是线程但又不完全是线程,我们去看看它的结构。
Worker 这个类的主要作用是,维护线程运行任务的中断控制状态和记录每个线程完成的任务数。
整体结构
1 |
|
我们来看看runWorker的实现。这个类主要的工作就是,不停地从阻塞队列中获取任务并执行,若firstTask不为空,就直接执行它。
1 |
|
🐱🏍 启动一个线程,大致执行的方法流程
getTask,我们来看看它是怎样阻塞或定时等待任务的。
Performs blocking or timed wait for a task, depending on current configuration settings, or returns null if this worker must exit because of any of:
- There are more than maximumPoolSize workers (due to a call to setMaximumPoolSize).
- The pool is stopped.
- The pool is shutdown and the queue is empty.
-
This worker timed out waiting for a task, and timed-out workers are subject to termination (that is, allowCoreThreadTimeOut workerCount > corePoolSize) both before and after the timed wait, and if the queue is non-empty, this worker is not the last thread in the pool. 🐱🏍(**超时等待任务的worker,在定时等待前后都会被终止(情况有,allowCoreThreadTimeOut wc > corePoolSize**)
Returns: task, or null if the worker must exit, in which case workerCount is decremented(worker退出时,workerCount会减一)
1 |
|
我们来看看当getTask返回null时,线程池是如何处理worker退出的
根据runWorker的代码,getTask为null,循环体正常退出,此时completedAbruptly = false;
processWorkerExit
1 |
|
getTask是保证存在的线程不被销毁的核心,getTask则利用阻塞队列的take方法,一直阻塞直到获取到任务为止。
当大于corePoolSize时,需要入队
1 |
|
队列已满
1 |
|
我们就讲完了执行方法execute(),有兴趣的同学可以去看看关闭方法shutdown()以及shutdownNow(),看看他们的区别。当然也可以去研究一下其他方法的源码。
探究一些小问题
- runWorker为啥这样抛错
1 |
|
We separately handle RuntimeException, Error (both of which the specs guarantee that we trap) and arbitrary Throwables. Because we cannot rethrow Throwables within Runnable.run, we wrap them within Errors on the way out (to the thread’s UncaughtExceptionHandler). Any thrown exception also conservatively causes thread to die.
大致意思就是,分别处理RuntimeException、Error和任何的Throwable。因为不能在 Runnable.run 中重新抛出 Throwables,所以将它们包装在 Errors中(到线程的 UncaughtExceptionHandler). 在Runnable.run不能抛出Throwables的原因是,Runnable中的run并没有定义抛出任何异常,继承它的子类,抛错的范围不能超过父类 UncaughtExceptionHandler可以处理“逃逸的异常”,可以去了解一下。
-
创建线程池最好手动创建,参数根据系统自定义 图中的设置线程数的策略只是初步设置,下一篇我们去研究具体的线程数调优
-
为什么创建线程开销大 启动一个线程时,将涉及大量的工作
- 必须为线程堆栈分配和初始化一大块内存。
- 需要创建/注册native thread在host OS中
- 需要创建、初始化描述符并将其添加到 JVM 内部数据结构中。
虽然启动一个线程的时间不长,耗费的资源也不大,但有个东西叫”积少成多”。就像 Doug Lea写的源码一样,有些地方的细节优化,看似没必要,但是请求一多起来,那些细节就是”点睛之笔”了。 当我们有大量需要线程时且每个任务都是独立的,尽量考虑使用线程池
总结
线程池的总体流程图
线程池新建线程,如何保证可以不断地获取任务,就是通过阻塞队列(BlockingQueue)的take方法,阻塞自己直到有任务才返回。
本篇博客也到这里就结束了,学习线程池以及输出博客,中间也拖了很久,最后送给大家以及自己最近看到的一句话
往往最难的事和自己最应该做的事是同一件事
参考
- Why is creating a Thread said to be expensive? 创建线程为何开销较大
- Java线程池实现原理及其在美团业务中的实践 讲了线程池的原理以及在美团的一些实际运用
- 10问10答:你真的了解线程池吗? 一些使用线程池的建议