线程池在java编程语言中的重要性就不言而喻了,对于线程池底层的实现可能大家的研究就没有那么深入了,下面针对线程池ThreadPoolExecutor中的最重要的一个成员变量ctl,来做一个介绍!
先看一下注释,原文如下
/** * The main pool control state, ctl, is an atomic integer packing * two conceptual fields * workerCount, indicating the effective number of threads * runState, indicating whether running, shutting down etc */
简单翻译一下,就是 ctl是一个线程安全的int变量,用于存储了两个概念的字段,第一个是当前线程池中的工作线程数,另外一个是当前线程池的状态。
* RUNNING: Accept new tasks and process queued tasks 接受新任务并处理队列中的任务 * SHUTDOWN: Don't accept new tasks, but process queued tasks 不接受新任务,但处理队列中的任务 * STOP: Don't accept new tasks, don't process queued tasks, * and interrupt in-progress tasks 不接受新任务,不处理队列中的任务,并中断正在进行的任务 * TIDYING: All tasks have terminated, workerCount is zero, * the thread transitioning to state TIDYING * will run the terminated() hook method 所有任务都已终止,workerCount为零,过渡到TIDYING状态的线程将运行terminated()钩子方法 * TERMINATED: terminated() has completedterminated()已完成
那么问题来了,用一个int变量怎么同时表示两个业务含义的数据呢?
先看下线程池中五个状态位是怎么定义的吧
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;
因为线程池作为一个稳定的常用工具,其状态不会频繁的变动,5种状态已经能完成其生命周期内的所有工作了,想一想,5种状态可以用多少位二进制表示?很显然大于5的最小的2的整数次幂是8,即用3位bit就能表示线程的状态了。
int在java中是占用4个字节,一个字节8bit,也就是一个int占用32bit,已经可以用3位bit来表示,那么剩下的29bit就是用来表示工作线程数了,那么,是怎么做到的呢?
这个毫无疑问是29了,因为int是占用32个二进制位,32-3就是29了
这里用到了<<左移运算,将1向左移动29位,可以想象下,得到的结果是不是这样
1000....000 【1后面跟着29个0】,对应的数值就是2的29次幂
然后,对结果减一,是不是得到的结果就是
111.....1111【29个1】
因为int是32位bit,补齐就是
000 1111.....11111 【29个1】
对应的大小是2的29次幂,说明在线程池中,最大能创建的线程的个数是2的29次幂,而并非是int的最大值。
这里补充一点知识,在计算机中数字存储的是其对应的补码,那么补码是怎么计算得出的呢?
先计算出原码,然后根据原码计算出反码,最后再根据反码得出补码,之所以计算机中要使用补码是因为如下两个原因
1.方便加减运算
2.使用补码,只有一个0,如果使用原码,将会有+0和-0,将会给计算带来复杂性
正数的原码,反码,补码一样
负数的原码,首位是1表示符号位,反码是保留原码的符号位,剩下的位取反操作,得到的结果就是反码,然后将反码+1就得到了补码
可以看到-1的补码就是32个1,然后左移29位,得到的结果就是,结果肯定是一个负数
111000...000【29个0】
毫无疑问,对应的shutdowm的二进制是
000000....000【32个0】
这个也很简单,对应的二进制是
001 000....000【连续29个0】
这个也很简单,对应的二进制是
010 000....000【连续30个0】
这个也很简单,对应的二进制是
011 000....000【连续29个0】
从这里可以看到,这个设计的目的就是用int的高三位来表示线程池的状态为,那么怎么根据一个int数值来判断当前线程池的状态呢?
private static int runStateOf(int c) { return c & ~CAPACITY; }
解释下,
CAPACITY的补码是000 1111.....11111 【29个1】
先对~CAPACITY取反,很显然得到的结果是
111 000...000【29个0】
& c
结果就很明了,因为CAPACITY取反后低29位都是0,只比较高三位,高三位和线程池中哪一个状态位相同,即当前线程池的状态
这个也是通过位运算来实现的,看下代码
private static int workerCountOf(int c) { return c & CAPACITY; }
解释下
直接用CAPACITY的补码
000 1111.....11111 【29个1】
& c
结果就很明了,因为CAPACITY的高3位是0,&运算有效的就是低29位,而低29位就是当前线程池中的工作线程数了
既然,一个int同时表示了两个业余含义,恳请需要有一个关联的方法,比如,当前线程池的状态是RUNNING,并且工作线程数是3个,怎么表示呢?
private static int ctlOf(int rs, int wc) { return rs | wc; }
解释下
|运算,你可以理解为10进制中的加法运算
比如上面的例子中就可以将
rs【线程池的状态RUNNING】111000...000【29个0】
|
wc【工作线程数3】000....0011【连续30个0】
得到的结果就是
111000....011【中间连续27个0】
至此,ctl的设计已经清晰明了了,通过使用一个int就能同时表示两个业务变量,不得不对Doug Lea老爷子表示敬意!
通过上面案例的分析,想必大家对线程池中的ctl设计已经有所了解了。
不过,在实际的业务开发中,我们还是要尽量避免这种写法,因为这样操作可能减少了变量,但是却增加了业务代码的复杂度,我们要好好思量下,一定要做好权衡!
如果你是中间件、通用组件开发,对时间复杂度和空间复杂度有相当高的要求,那自然另当别论了,另外,这种用二进制位存储的思想,还可以用来表示一个业务同时存在的属性,详情参考利用位运算实现一个字段表示多个属性-CSDN博客