内容简介:小豹子带你看源码:Java 线程池(二)实例化
我们首先看构造器的声明, 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() { @Override public Thread newThread(Runnable r) { return new Thread(); } }, new RejectedExecutionHandler() { @Override public 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
中剥离出运行状态值和线程数。接下来我着重解释一下 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中线程安全,线程死锁,线程通信快速入门
- ObjC 多线程简析(一)-多线程简述和线程锁的基本应用
- Java多线程之线程中止
- Android 的线程和线程池
- iOS 多线程之线程安全
- java多线程 线程安全问题
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
程序是怎样跑起来的
[日] 矢泽久雄 / 李逢俊 / 人民邮电出版社 / 2015-4 / 39.00元
本书从计算机的内部结构开始讲起,以图配文的形式详细讲解了二进制、内存、数据压缩、源文件和可执行文件、操作系统和应用程序的关系、汇编语言、硬件控制方法等内容,目的是让读者了解从用户双击程序图标到程序开始运行之间到底发生了什么。同时专设了“如果是你,你会怎样介绍?”专栏,以小学生、老奶奶为对象讲解程序的运行原理,颇为有趣。本书图文并茂,通俗易懂,非常适合计算机爱好者及相关从业人员阅读。一起来看看 《程序是怎样跑起来的》 这本书的介绍吧!