线程池系列 - (2)线程池的状态

·  阅读 773

前言

本想着写一篇博客就将线程池全部分析完的,后来发现。线程池有很多值得玩味的地方。所以笔者将分成几个系列慢慢的分析。

本篇文章主要分析一下线程池的状态。废话不多说。上车走起

系列文章

五种运行状态

在线程池中可以看到。其内部定了了5种状态。如下所示。

借助Java线程池实现原理及其在美团业务中的实践中,对每一种状态的解释,如下图所示。

其状态转化过程如下

如何维护状态

知道了这几个状态,那么线程池中是如何记录这个状态的呢。这里就是线程池设计的巧妙之处。

线程池中ctl一个变量中就保存了线程池的运行状态(runState)和线程数量(workerCount)。

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
复制代码

我们知道。一个int 占4个字节。32位。线程池使用一个int 中前3位作为运行状态(runState),后29位作为线程数量(workerCount)。那线程池是如何进行运算去使用一个变量就保存了两个状态呢。

首先我们知道了一个概念 前面3位是状态。后面29位是线程的数量。在代码中也能看到,线程池内部的定义。如下COUNT_BITS最终的运算就是29.

//Integer.SIZE = 32
//COUNT_BITS = 29
private static final int COUNT_BITS = Integer.SIZE - 3;
复制代码

知道了一些概念性的东西。我们回过头在来看一下线程池的状态。笔者整理了一个表格如下。主要看前三位。

状态计算方式计算结果前三位
RUNNING-1 << COUNT_BITS1110 0000 0000 0000 0000 0000 0000 0000111
SHUTDOWN0 << COUNT_BITS0000 0000 0000 0000 0000 0000 0000 0000000
STOP1 << COUNT_BITS0010 0000 0000 0000 0000 0000 0000 0000001
TIDYING2 << COUNT_BITS0100 0000 0000 0000 0000 0000 0000 0000010
TERMINATED3 << COUNT_BITS0110 0000 0000 0000 0000 0000 0000 0000011

CAPACITY如何理解

下图是线程池内部计算线程数量。线程状态以及更新状态的方法。可以看到 runStateofworkerCountOf都是和CAPACITY进行运算。

那么CAPACITY是什么呢。在下图中。我们看到。线程池中就能看到。CAPACITY就是一个int.

Ok~.将代码拿出来。我们分析一下这么一行代码。看一下。这个倒是是个啥。

private static final int CAPACITY   = (1 << COUNT_BITS) - 1;
复制代码

首先看到1 << COUNT_BITS,我们知道 COUNT_BITS其实是29,那么1 << COUNT_BITS的结果2进制应该是

0010 0000 0000 0000 0000 0000 0000 0000
复制代码

在减去1那么其二进制就应该是

0001 1111 1111 1111 1111 1111 1111 1111
复制代码

放上图的话。会不会于目了然呢

1 << COUNT_BITS(1 << COUNT_BITS) - 1

eumm 最终会得到一个 后面29为都是1 前面3为都是0 的这么一个数。

计算线程池数量

知道了CAPACITY是个啥。我们再看一下是如何计算线程数量的

private static int workerCountOf(int c)  { return c & CAPACITY; }
复制代码

我们知道CAPACITY 的前3位都是0.后29位都是1.任何数与0与都是0.就相当于去了后面29位。是不是很巧妙呢。

计算线程状态

在看如何计算线程状态的。

private static int runStateOf(int c)     { return c & ~CAPACITY; }
复制代码

也是非常巧妙的设计。先将CAPACITY进行非操作

原本是

0001 1111 1111 1111 1111 1111 1111 1111
复制代码

现在变成了

1110 0000 0000 0000 0000 0000 0000 0000
复制代码

然后在进行与操作。就相当于取了前3位。eumm 秒~

更新线程池状态和线程数量

private static int ctlOf(int rs, int wc) { return rs | wc; }
复制代码

例如在开始的线程池初始化的时候.进行了ctlOf(RUNNING, 0)

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
复制代码

看一下其计算过程。两个位只要有一个为1,那么结果就是1,否则就为0

1110 0000 0000 0000 0000 0000 0000 0000

0000 0000 0000 0000 0000 0000 0000 0000
复制代码

计算结果如下

1110 0000 0000 0000 0000 0000 0000 0000
复制代码

所以线程池创建的时候。其状态是RUNNING,线程的数量是0

如果在线程池工作状态时候。线程数量发生了变化。可不是通过ctlOf方法去更新的哈。有专门的方法去更新.

private boolean compareAndIncrementWorkerCount(int expect) {
    return ctl.compareAndSet(expect, expect + 1);
}
复制代码

为什么这么设计呢

为什么搞那么麻烦。用一个变量去维护两个状态。笔者也总结了一下前人们的经验。

  • 如果是两个变量。那么就需要多占有资源。
  • 两个变量更加难以维护。在多线程中。为了维护两个变量。势必会占有锁资源。
  • 线程池中很多地方都是需要同时判断线程状态和线程数量。这么设计也是为了防止出现判断时候发生不可预期的状态不一致的情况。

还有一个细节。ThreadPoolExecutor中定义了一个变量ctl,而他的类型是AtomicInteger。这个原子性的类保证了线程安全。不会因为多线程而导致错误。

状态如何切换

前人栽树,后人乘凉。下面的图就是前人的总结。笔者这里直接放上

在上面笔者举了一个🌰。在初始化的时候同步了状态。代码如下。

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
复制代码

通过代码以及前面如何同步状态。我们知道。此时的线程数量是0.线程状态就是RUNNING.

在上面的流程当中其实可以看到。状态改变。在执行shutdownshutdownNow的时候是发生不同的。关于这两个方法。笔者之后会专门在介绍。

先看一下shutdown方法

这里看到advanceRunState(SHUTDOWN); 方法。点击进入就能看到,其使用ctlOf方法更新了状态

advanceRunState 方法源码
private void advanceRunState(int targetState) {
    // assert targetState == SHUTDOWN || targetState == STOP;
    for (;;) {
        int c = ctl.get();
        if (runStateAtLeast(c, targetState) ||
            ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))))
            break;
    }
}
复制代码

再来看一下tryTerminate方法。笔者下面框选的就是其状态更新的地方。

再看一下shutdownNow 在框选中的状态和 shutdown是不同的。

源码中哪些地方用到了状态判断

有人就说了,你说的我都懂。那在哪进行判断呢。判断的作用是什么。

首先。线程池也不想想的那么复杂。作为一个池子。他最大的作用就是复用线程。线程池内部的状态。就是为了判断当前的池子是否可用。即是否是在running状态。如果不是这个状态。就说明线程池是不可用的。不能用线程池咋办啊。简单啊。不可用就不给用。线程池有自己的拒绝策略。默认就是抛出一个异常。在源码中也能看到

最后

本文主要还是介绍状态为主。线程池说难也难。说简单也简单。笔者依然会从不同的点去分析与总结。当然文中有错误的观点。欢迎批评指正。

分类:
Android
标签: