承上启下:上一篇文章小豹子讲了我为什么想要研究线程池的代码,以及我计划要怎样阅读代码。这篇文章我主要阅读了线程池实例化相关的代码,并提出了自己的疑问。

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;
复制代码

又是一堆天书,但似乎 RUNNINGSHUTDOWN 等是表示某种状态的常量,至于它们的赋值为什么这么特殊,其他变(常)量都是干嘛的?老套路,看文档。

文档告诉我们: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_BITSInteger 的长度减 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 的线程池状态。

那么 runStateOfworkerCountOf 就不必多说,是从 ctl 中剥离出运行状态值和线程数,在这里 CAPACITY 的作用就体现出来,它表示一种标志位,因为它二进制值的特性(前文提到)使得另一个值与它进行位与(或非与)运算时可以得到值的低位(或高位)。接下来我着重解释一下 isRunning(int c),首先我们要已知两个事实:

  1. 运行状态值之间的顺序很重要,运行状态的值随时间单调递增,RUNNING 是最小的,SHUTDOWN 次之。
  2. 运行状态储存在 ctl 变量的高三位。

那么判断当前线程池的状态是否为 RUNNING,有没有必要将 ctl 中的状态值提取出来,再与 RUNNING 常量进行对比呢?没有必要,因为状态值占高位,只要状态值小于 SHUTDOWNctl 就必然小于 SHUTDOWN,而小于 SHUTDOWN 的状态只有 RUNNING,因此只要 ctl 值小于 SHUTDOWN,它就一定是 RUNNING 状态。其他函数(runStateLessThanrunStateAtLeast)同理,直接对比就好。

3.3 疑问

看到 ThreadPoolExecutor 中用一个原子变量存储两种状态的设计思想,我心中产生一个疑问:为什么要这样做?为了节省内存么?肯定不是,线程池的主要应用场景应该是服务器,而用时间换空间(还只换了这么点空间)是非常不值得的。那么我唯一能想到的解释是,有利于提高并发性能。

我记得我在看《高性能 MySQL》的时候,作者告诉我这样一种思想:热点分离。

书中描绘了这样一个应用场景,一个类似微博的应用,后台要统计总发贴数。那么每一次获取数据都要 count(*) 这肯定不现实。现实一点的做法是,在数据库中维护一个表示总发贴数的记录,每一次用户发帖,这个值就加 1。这种方案并发性能也不是很好。因为这个字段至少要加行锁,每次用户发帖,总发贴数加 1 时都会引起锁竞争。这相当于把用户发帖行为串行化了。

书中的解决方案是设计一张表,其中有 n 条记录(比如说 100 条),每一次用户发帖,在这 100 条记录中选一条记录(可以是随机选择,也可以根据时间取模)自加 1。然后每隔一段时间将表中的所有记录加和赋值到第一条记录中,删除其他记录。这样一来,原先是 N 个线程争抢一把锁,现在是 N 个线程争抢一百把锁。并发性能当然得到了增加。这就是所谓的热点分离。

ThreadPoolExecutorctl 的设计似乎反其道而行之。把两个需要并发访问的值“捏”到了一起。除非运行状态和线程数往往同时变化,否则这样做,我理解不了它是怎样提高并发性能的。我决定暂时搁置这个问题,在后续对源码的学习过程中,我相信我能得到答案。

系列文章

  • Java 线程池(一)缘起 & 计划
  • Java 线程池(二)实例化
  • Java 线程池(三)提交任务
  • 未完待续……

小豹子还是一个大三的学生,小豹子希望你能“批判性的”阅读本文,对本文内容中不正确、不妥当之处进行严厉的批评,小豹子感激不尽。

小豹子带你看源码:Java 线程池(二)实例化相关推荐

  1. 小豹子带你看源码:Java 线程池(三)提交任务

    承上启下:上一篇文章小豹子讲了线程池的实例化过程,粗略介绍了线程池的状态转换:这篇文章主要讲了我运行线程池时遇到的小问题,以及 execute 方法的源码理解. 4 并不算疑难的 Bug 按照我们的规 ...

  2. java 缘起_小豹子带你看源码:Java 线程池(一)缘起 计划

    1 缘起 怎么想起来看线程池的代码? 很简单,因为我不会用. 原先遇到用线程池一直是 Executors 直接构造一个出来.啊,newFixedThreadPool 就是创建定容线程池,线程数是固定的 ...

  3. 小豹子带你看源码:ArrayList

    世界上最牛的 Java 代码去哪找?当然是 JDK 咯-计划学习一下常见容器的源码. 我会把我觉得比较有意思或者玄学的地方更新到这里. 以下 JDK 源码及 Javadoc 均从 java versi ...

  4. JUC源码分析-线程池篇(五):ForkJoinPool - 2

    通过上一篇(JUC源码分析-线程池篇(四):ForkJoinPool - 1)的讲解,相信同学们对 ForkJoinPool 已经有了一个大概的认识,本篇我们将通过分析源码的方式来深入了解 ForkJ ...

  5. Elasticsearch源码分析—线程池(十一) ——就是从队列里处理请求

    Elasticsearch源码分析-线程池(十一) 转自:https://www.felayman.com/articles/2017/11/10/1510291570687.html 线程池 每个节 ...

  6. 菜鸟带你看源码——看不懂你打我ArrayList源码分析(基于java 8)

    文章目录 看源码并不难 软件环境 成员变量: 构造方法 核心方法 get方法 remove方法 add方法 结束 看源码并不难 如何学好编程?如何写出优质的代码?如何快速提高自己的编程能力?等等一系列 ...

  7. 深读源码-java线程系列之自己手写一个线程池

    问题 (1)自己动手写一个线程池需要考虑哪些因素? (2)自己动手写的线程池如何测试? 简介 线程池是Java并发编程中经常使用到的技术,那么自己如何动手写一个线程池呢?本文将手把手带你写一个可用的线 ...

  8. java 线程池 源码_java线程池源码分析

    我们在关闭线程池的时候会使用shutdown()和shutdownNow(),那么问题来了: 这两个方法又什么区别呢? 他们背后的原理是什么呢? 线程池中线程超过了coresize后会怎么操作呢? 为 ...

  9. 从源码学习线程池的使用原理及核心思想解析

    文章内容引用自 咕泡科技 咕泡出品,必属精品 文章目录 1为什么要使用线程池 2几种常用线程池介绍 3从初始化开始 4执行任务execute 5添加线程addWorker 6运行新的线程runWork ...

最新文章

  1. K-means算法(理论+opencv实现)
  2. 中国联通沈阳互联网数据中心
  3. Oracle PL SQL 精萃pdf
  4. 40个精心设计的免费的社交网络图标
  5. new,is和as运算符解析及运行时类型,对象,线程堆栈,托管堆之间的联系
  6. storm UI解释
  7. 扯淡!C语言怎么可能被淘汰呢?
  8. 如何给女朋友解释什么是撞库、脱库和洗库?
  9. 在TensorFlow中使用pipeline加载数据
  10. iPhone 12发布前夜:富士康奖励万元招不到人,有工人国庆连轴转
  11. 曾经我也是运营着两个淘宝店铺的小卖家
  12. 51nod 2494 最长配对
  13. 服务器启动socket服务报错 java.net.BindException:Cannot assign requested address
  14. 【修电脑】电脑将在1分钟后重启
  15. python3  类中方法的调用
  16. 在odl中如何实现rpc
  17. 辛普森法 matlab,MATLAB辛普森法则
  18. 全球与中国泄漏吸收枕头市场深度研究分析报告
  19. sqlalchemy 踩过的坑
  20. 手机WAP网站 相关

热门文章

  1. java按钮按行放置_java 放置按钮
  2. 【Linux】多线程下载工具axel的安装和使用
  3. 微信小程序导入demon
  4. jsp超市会员积分管理系统
  5. 在OC项目中添加Swift文件并实现混合编程
  6. unity 卡牌聚拢算法
  7. 昇思MindSpore携手宝兰德推出智慧工地解决方案,助力工地安全生产管理领域数智化升级
  8. SS54/SS24/SS510及SMA/SMB/SMC不同封装区别
  9. matlab中kmeans聚类算法
  10. SNR、BER、Eb/N0之间的区别与联系