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

栏目: Java · 发布时间: 6年前

内容简介:小豹子带你看源码: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;

又是一堆天书,但似乎 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 中剥离出运行状态值和线程数。接下来我着重解释一下 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 线程池(二)实例化》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

程序是怎样跑起来的

程序是怎样跑起来的

[日] 矢泽久雄 / 李逢俊 / 人民邮电出版社 / 2015-4 / 39.00元

本书从计算机的内部结构开始讲起,以图配文的形式详细讲解了二进制、内存、数据压缩、源文件和可执行文件、操作系统和应用程序的关系、汇编语言、硬件控制方法等内容,目的是让读者了解从用户双击程序图标到程序开始运行之间到底发生了什么。同时专设了“如果是你,你会怎样介绍?”专栏,以小学生、老奶奶为对象讲解程序的运行原理,颇为有趣。本书图文并茂,通俗易懂,非常适合计算机爱好者及相关从业人员阅读。一起来看看 《程序是怎样跑起来的》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具