小豹子带你看源码:Java 线程池(二)实例化
承上启下:上一篇文章小豹子讲了我为什么想要研究线程池的代码,以及我计划要怎样阅读代码。这篇文章我主要阅读了线程池实例化相关的代码,并提出了自己的疑问。
3 千里之行,始于实例化
3.1 先创建一个线程池玩玩
我们首先看构造器的声明,ThreadPoolExecutor
有四个重载构造器,其中三个分别指定了不同的缺省参数值,我们直接看参数最全的构造器:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
复制代码
参数有点多,我们有点懵,但并不是无从下手。我们去看代码上方的 JavaDoc:
- corePoolSize:要保留在池中的线程数。即便线程空闲,不小于该参数的线程也将被保留。除非设置了
allowCoreThreadTimeOut
- maximumPoolSize:池中允许的最大线程数
- keepAliveTime:当池中线程数大于核心池数量(
corePoolSize
)时,大于核心池数量部分的线程空闲持续keepAliveTime
时间后,将被终止 - unit:
keepAliveTime
参数的时间单位 - workQueue:在任务被执行之前用于保存任务的队列。这个队列只包含由
execute
方法提交的Runnable
任务 - threadFactory:
executor
创建新线程时使用的线程工厂 - handler:用于处理由于超过线程上限或队列上限而产生的拒绝服务异常
那么我们根据文档来创建一个线程池:
@Test
public void newInstanceTest() {ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), new ThreadFactory() {@Overridepublic Thread newThread(Runnable r) {return new Thread();}}, new RejectedExecutionHandler() {@Overridepublic void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {System.out.println("拒绝服务");}});
}
复制代码
这里我们创建了一个核心池数量为 5,最大线程数为 10,线程保持时间为 60 秒的线程池。
3.2 初始化时,线程池做了什么?
我们跟踪到代码中,看实例化的过程中,构造器为我们做了什么:
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.acc = System.getSecurityManager() == null ?null :AccessController.getContext();this.corePoolSize = corePoolSize;this.maximumPoolSize = maximumPoolSize;this.workQueue = workQueue;this.keepAliveTime = unit.toNanos(keepAliveTime);this.threadFactory = threadFactory;this.handler = handler;
}
复制代码
这里很容易理解,前面进行了输入参数的检查,this.acc
是访问控制器上下文,这里我们不深入研究它。唯一值得一提的就是 unit.toNanos(keepAliveTime)
,这是将参数中的 keepAliveTime
转换成纳秒,似乎也不难理解,但我有一个疑问:为什么要抽象时间单位?抽象时间段不好么?比如我设计一个 Period
类表示一段时间,里面有几个静态方法用于实例化,比如 Period.fromSeconds(long n)
表示 n
秒的一段时间,然后可以使用 Period#toNanos()
这类的方法将该段时间传化为纳秒。这样可以是参数更简洁,表意更明确。不知两种设计方案的优缺,还望各位指点。
我们继续看 ThreadPoolExecutor
的初始化:
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
复制代码
又是一堆天书,但似乎 RUNNING
、SHUTDOWN
等是表示某种状态的常量,至于它们的赋值为什么这么特殊,其他变(常)量都是干嘛的?老套路,看文档。
文档告诉我们:ctl
是表示线程池状态的原子整形,它包含两部分:工作线程数、运行状态。为了将两个变量用一个原子整形表示,我们限制工作线程数最多只能有 (2^29)-1
(大概 5 亿)个,而空余的高三位用来存储运行状态。
运行状态可能有这些值:
- RUNNING:允许提交新任务,处理队列中的任务
- SHUTDOWN:不允许提交新任务,但处理队列中的任务
- STOP:不允许提交新任务,不处理队列中的任务,打断执行中的任务
- TIDYING:所有任务已经终止,工作线程数为零,线程过渡到
TIDYING
时将调用terminated()
回调方法 - TERMINATED:
terminated()
方法完成后
这些值之间的顺序很重要,运行状态的值随时间单调递增,但在一个生命周期内不需要经历过所有的状态。
状态的转换:
- RUNNING -> SHUTDOWN:调用
shutdown()
触发,或者隐含在finalize()
中 - (RUNNING / SHUTDOWN) -> STOP:调用
shutdownNow()
触发 - SHUTDOWN -> TIDYING:当队列和池均为空时触发
- STOP -> TIDYING:当池为空时触发
- TIDYING -> TERMINATED:
terminated()
执行结束之后
看过文档之后,我们再回头看这几个常量的赋值:首先 COUNT_BITS
是 Integer
的长度减 3,其他几个状态量分别是 -1、0、1,2,3 向高位移动 COUNT_BITS
位的结果,这也就对应着文档所写,用一个整形的高三位来存储线程池的状态。CAPACITY
的值是 1 向高位移动 COUNT_BITS
位再减一,字面意思是容量,这不难理解,COUNT_BITS
就是代表线程池所能容纳的最大线程数,而值得一提的是,这个值在二进制层面上具有另一个意义:CAPACITY
的二进制值高三位为 0,其他位为 1。具体用途,我们后面细说。
现在只剩 ctl
我们不清楚了,首先从文档中我们可以获知 ctl
是包含了运行状态与线程数量的一个整形原子变量,那么 ctlOf(RUNNING, 0)
是什么意思呢?我们来看 ThreadPoolExecutor
中的静态方法:
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }private static boolean runStateLessThan(int c, int s) {return c < s;
}private static boolean runStateAtLeast(int c, int s) {return c >= s;
}private static boolean isRunning(int c) {return c < SHUTDOWN;
}
复制代码
这里小豹子带大家回忆一下位运算:
&
是按位与运算符,输入均为 1 输出为 1,其他为 0;
|
是按位或运算符,输入均为 0 输出为 0,其他为 1;
~
是按位非运算符,输入为 0 输出为 1,输入为 1 输出为 0;
我们看 ctlOf(int rs, int wc)
,其中 rs 指运行状态(runState),wc 值线程数(workerCount)。rs 值的特点是高三位表示运行状态,而其他低位均为 0,wc 值的特点是高三位为 0(因为不大于 CAPACITY
嘛),低位表示线程数。那么对两个值进行按位或运算,正好就将两个值的有效位合并到一个整形变量中。我们再回头看 ctl
变量的初始化 new AtomicInteger(ctlOf(RUNNING, 0))
。这回应该就清楚了,ctlOf(RUNNING, 0)
表示运行状态是 RUNNING
,线程数为 0 的线程池状态。
那么 runStateOf
与 workerCountOf
就不必多说,是从 ctl
中剥离出运行状态值和线程数,在这里 CAPACITY
的作用就体现出来,它表示一种标志位,因为它二进制值的特性(前文提到)使得另一个值与它进行位与(或非与)运算时可以得到值的低位(或高位)。接下来我着重解释一下 isRunning(int c)
,首先我们要已知两个事实:
- 运行状态值之间的顺序很重要,运行状态的值随时间单调递增,
RUNNING
是最小的,SHUTDOWN
次之。 - 运行状态储存在
ctl
变量的高三位。
那么判断当前线程池的状态是否为 RUNNING
,有没有必要将 ctl
中的状态值提取出来,再与 RUNNING
常量进行对比呢?没有必要,因为状态值占高位,只要状态值小于 SHUTDOWN
,ctl
就必然小于 SHUTDOWN
,而小于 SHUTDOWN
的状态只有 RUNNING
,因此只要 ctl
值小于 SHUTDOWN
,它就一定是 RUNNING
状态。其他函数(runStateLessThan
、runStateAtLeast
)同理,直接对比就好。
3.3 疑问
看到 ThreadPoolExecutor
中用一个原子变量存储两种状态的设计思想,我心中产生一个疑问:为什么要这样做?为了节省内存么?肯定不是,线程池的主要应用场景应该是服务器,而用时间换空间(还只换了这么点空间)是非常不值得的。那么我唯一能想到的解释是,有利于提高并发性能。
我记得我在看《高性能 MySQL》的时候,作者告诉我这样一种思想:热点分离。
书中描绘了这样一个应用场景,一个类似微博的应用,后台要统计总发贴数。那么每一次获取数据都要 count(*)
这肯定不现实。现实一点的做法是,在数据库中维护一个表示总发贴数的记录,每一次用户发帖,这个值就加 1。这种方案并发性能也不是很好。因为这个字段至少要加行锁,每次用户发帖,总发贴数加 1 时都会引起锁竞争。这相当于把用户发帖行为串行化了。
书中的解决方案是设计一张表,其中有 n 条记录(比如说 100 条),每一次用户发帖,在这 100 条记录中选一条记录(可以是随机选择,也可以根据时间取模)自加 1。然后每隔一段时间将表中的所有记录加和赋值到第一条记录中,删除其他记录。这样一来,原先是 N 个线程争抢一把锁,现在是 N 个线程争抢一百把锁。并发性能当然得到了增加。这就是所谓的热点分离。
但 ThreadPoolExecutor
中 ctl
的设计似乎反其道而行之。把两个需要并发访问的值“捏”到了一起。除非运行状态和线程数往往同时变化,否则这样做,我理解不了它是怎样提高并发性能的。我决定暂时搁置这个问题,在后续对源码的学习过程中,我相信我能得到答案。
系列文章
- Java 线程池(一)缘起 & 计划
- Java 线程池(二)实例化
- Java 线程池(三)提交任务
- 未完待续……
小豹子还是一个大三的学生,小豹子希望你能“批判性的”阅读本文,对本文内容中不正确、不妥当之处进行严厉的批评,小豹子感激不尽。
小豹子带你看源码:Java 线程池(二)实例化相关推荐
- 小豹子带你看源码:Java 线程池(三)提交任务
承上启下:上一篇文章小豹子讲了线程池的实例化过程,粗略介绍了线程池的状态转换:这篇文章主要讲了我运行线程池时遇到的小问题,以及 execute 方法的源码理解. 4 并不算疑难的 Bug 按照我们的规 ...
- java 缘起_小豹子带你看源码:Java 线程池(一)缘起 计划
1 缘起 怎么想起来看线程池的代码? 很简单,因为我不会用. 原先遇到用线程池一直是 Executors 直接构造一个出来.啊,newFixedThreadPool 就是创建定容线程池,线程数是固定的 ...
- 小豹子带你看源码:ArrayList
世界上最牛的 Java 代码去哪找?当然是 JDK 咯-计划学习一下常见容器的源码. 我会把我觉得比较有意思或者玄学的地方更新到这里. 以下 JDK 源码及 Javadoc 均从 java versi ...
- JUC源码分析-线程池篇(五):ForkJoinPool - 2
通过上一篇(JUC源码分析-线程池篇(四):ForkJoinPool - 1)的讲解,相信同学们对 ForkJoinPool 已经有了一个大概的认识,本篇我们将通过分析源码的方式来深入了解 ForkJ ...
- Elasticsearch源码分析—线程池(十一) ——就是从队列里处理请求
Elasticsearch源码分析-线程池(十一) 转自:https://www.felayman.com/articles/2017/11/10/1510291570687.html 线程池 每个节 ...
- 菜鸟带你看源码——看不懂你打我ArrayList源码分析(基于java 8)
文章目录 看源码并不难 软件环境 成员变量: 构造方法 核心方法 get方法 remove方法 add方法 结束 看源码并不难 如何学好编程?如何写出优质的代码?如何快速提高自己的编程能力?等等一系列 ...
- 深读源码-java线程系列之自己手写一个线程池
问题 (1)自己动手写一个线程池需要考虑哪些因素? (2)自己动手写的线程池如何测试? 简介 线程池是Java并发编程中经常使用到的技术,那么自己如何动手写一个线程池呢?本文将手把手带你写一个可用的线 ...
- java 线程池 源码_java线程池源码分析
我们在关闭线程池的时候会使用shutdown()和shutdownNow(),那么问题来了: 这两个方法又什么区别呢? 他们背后的原理是什么呢? 线程池中线程超过了coresize后会怎么操作呢? 为 ...
- 从源码学习线程池的使用原理及核心思想解析
文章内容引用自 咕泡科技 咕泡出品,必属精品 文章目录 1为什么要使用线程池 2几种常用线程池介绍 3从初始化开始 4执行任务execute 5添加线程addWorker 6运行新的线程runWork ...
最新文章
- K-means算法(理论+opencv实现)
- 中国联通沈阳互联网数据中心
- Oracle PL SQL 精萃pdf
- 40个精心设计的免费的社交网络图标
- new,is和as运算符解析及运行时类型,对象,线程堆栈,托管堆之间的联系
- storm UI解释
- 扯淡!C语言怎么可能被淘汰呢?
- 如何给女朋友解释什么是撞库、脱库和洗库?
- 在TensorFlow中使用pipeline加载数据
- iPhone 12发布前夜:富士康奖励万元招不到人,有工人国庆连轴转
- 曾经我也是运营着两个淘宝店铺的小卖家
- 51nod 2494 最长配对
- 服务器启动socket服务报错 java.net.BindException:Cannot assign requested address
- 【修电脑】电脑将在1分钟后重启
- python3 类中方法的调用
- 在odl中如何实现rpc
- 辛普森法 matlab,MATLAB辛普森法则
- 全球与中国泄漏吸收枕头市场深度研究分析报告
- sqlalchemy 踩过的坑
- 手机WAP网站 相关