巧用位运算(一)—— ThreadPoolExecutor源码解读

鸡汤

三月七日,沙湖道中遇雨。雨具先去,同行皆狼狈,余独不觉。已而遂晴,故作此词。
莫听穿林打叶声,何妨吟啸且徐行。竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生。
料峭春风吹酒醒,微冷,山头斜照却相迎。回首向来萧瑟处,归去,也无风雨也无晴。
                   《定风波·莫听穿林打叶声》苏轼

 位运算在我们平时开发中可能很少会遇到,但是巧用位运算能帮我们解决很多问题,并使得代码更加简洁,运行更加高效。

 在我之前写的一篇HashMap的秘密(另类角度源码解读)文章中,可以看到JDK使用位运算取代了取模运算,这是因为在指令层面取模运算的CPU周期可能是位运算的数倍。

 为了更好的认识位运算带来的好处,我们思考下面一个问题:

 怎样判断一个正整数是否是2的指数幂?

 这个问题要用算术运算来解决可能代码会是下面这样:

    private static boolean is_Pow(int n) {
        if (n % 2 == 0) {
            if (n / 2 == 1) return true;
            return is_Pow(n / 2);
        }
        return false;
    }

  但是如果使用位运算,则只需要一行代码:

    private static boolean is_Pow2(int n) {
        return (((n - 1) & n) == 0);
    }

 本篇不涉及任何位运算符,二进制与十六进制转换的解释,如果对位运算不了解的朋友可以先去了解一下位运算的基本操作,否则看起来可能不太容易适应。

 实际上任何优秀的开源项目都不可避免的会大量运用位运算来提升计算效率和压缩内存。Lucene的倒排表就使用了变长整型Vint来存储ID,从而压缩内存,而Vint的数据结构就是基于位运算产生的。同理Redis的简单动态字符串(SDS)也是使用位运算来进行长度判断的(这个我会在后续章节深入)。

ThreadPoolExecutor源码

 接下来我们就由浅入深看看JDK源码中线程池的位运算应用,进入到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;

    // runState is stored in the high-order bits
    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;

常量的定义

COUNT_BITS:位数,我们可以看到,在后面的常量定义中都使用到了这个COUNT_BITS常量。为什么这个位数会是29位呢(Integer是32位)?这个问题我们留待后面回答。
CAPACITY:顾名思义,这个数值代表了线程池的容量,赋值表达式(1 << COUNT_BITS) - 1,根据位运算我们可以得到结果00011111111111111111111111111111111
RUNNING: 代表线程池的工作状态,表示线程池正在正常运行中。表达式-1 << COUNT_BITS的计算结果是11100000000000000000000000000000
SHUTDOWN: 代表线程池的工作状态,表示线程池停止接收新任务,排队任务执行完毕后停止运行。表达式0 << COUNT_BITS的计算结果是00000000000000000000000000000000
STOP: 代表线程池的工作状态,表示线程池停止接收新任务并放弃排队任务,正在执行的任务完成后停止运行。表达式1 << COUNT_BITS的计算结果是00100000000000000000000000000000
TIDYING: 代表线程池的工作状态,表示线程池完成所有工作准备调用tryTerminate()。表达式2 << COUNT_BITS的计算结果是01000000000000000000000000000000
TERMINATED: 代表线程池的工作状态,表示线程池已经停止运行。表达式3 << COUNT_BITS的计算结果是01100000000000000000000000000000

线程池的五种状态关系

 实际上从上面的常量赋值我们可以看出,ThreadPoolExecutor使用了Integer低29位作为线程池容量的记录,使用高3位作为状态值的记录。这样使用一个32位的变量就同时记录了线程池的运行状态和线程池工作线程的数量,而这个变量就是我们上面看到的CAS原子整型ctl
 很多同学看到这里不经会想:“这样的设计不是自找麻烦吗,直接用两个Integer变量不就能很直观的表达了吗?”,下面我来谈谈我自己的一些看法。
ctl变量的设计我个人认为最主要是为了内存压缩。由于操作系统和物理机内存的限制,线程池的工作线程数量是不可能达到2^29,而线程池的状态值也只有5种,用3位二进制足够表达。所以如果我们使用两个Integer变量来分别记录线程数和状态,无疑会造成内存的浪费。虽然这里占用的内存微不足道,但是千里之堤溃于蚁穴,优秀的底层开发人员理所当然需要把空间的利用做到极致。
 同时由于位运算的操作,让我们可以轻松的实现对状态值和线程数的分开操作(而这种操作往往只需要消耗个位数的CPU周期,几乎不会造成性能上的太大损耗)。如下:

    //获取线程池状态
    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; }

扩展思路

 在了解了线程池的设计思想时,我们便可以思考一个问题:“如果现在线程池的状态由5种变成了10种,或者线程池的容量需要提升4倍,这时应该怎么设计”。如果仅仅是线程池的状态增加,我们可以通过计数指针后移的操作,即用Integer的高4位表示状态,低28位表示线程大小。这时状态的种类可以达到16种,而线程池的容量缩小了一倍,但仍是够用的。如果操作系统和物理机都支持的情况下,线程池容量还需要提升,则我们可以考虑使用Long型代替Integer来创建ctl。这些调整都不会使得代码出现大范围的改动。

注:本章内容并没有涉及到ThreadPoolExecutor中队列的处理以及锁实现。仅仅从ctl设计思路上加深大家对位运算的理解。希望大家能够多多借鉴诸如此类的设计思想,让自己的代码得到升华。

下章预告:【巧用位运算(二)—— 变长整型的实现】
简介: 大名鼎鼎的ElasticSearch就是基于Lucene搜索引擎实现了分布式搜索,我们都知道ES之所以快,一方面是因为Lucene是基于内存的索引存储,所以Lucene是非常吃内存的。Lucene在实现中总是不可避免的会考虑到内存的压缩和利用,就用到了诸如Roaring BitmapsvIntFrame Of Reference等存储技巧。下一章我们会自己实现变长整型vInt,带大家更好的理解位运算在程序设计中的重大意义。

你可能感兴趣的:(巧用位运算(一)—— ThreadPoolExecutor源码解读)