线程池的状态-操作-问题

1、线程池状态

首先线程池是有状态的,这些状态标识这线程池内部的一些运行情况,线程池的开启到关闭的过程就是线程池状态的一个流转的过程。

线程池共有五种状态:

状态含义
RUNNING运行状态,该状态下线程池可以接受新的任务,也可以处理阻塞队列中的任务
执行 shutdown 方法可进入 SHUTDOWN 状态
执行 shutdownNow 方法可进入 STOP 状态
SHUTDOWN待关闭状态,不再接受新的任务,继续处理阻塞队列中的任务
当阻塞队列中的任务为空,并且工作线程数为0时,进入 TIDYING 状态
STOP停止状态,不接收新任务,也不处理阻塞队列中的任务,并且会尝试结束执行中的任务
当工作线程数为0时,进入 TIDYING 状态
TIDYING整理状态,此时任务都已经执行完毕,并且也没有工作线程
执行 terminated 方法后进入 TERMINATED 状态
TERMINATED终止状态,此时线程池完全终止了,并完成了所有资源的释放

2、线程池的一些属性

2.1、线程状态和工作线程数量

首先线程池是有状态的,不同状态下线程池的行为是不一样的,5种状态已经在上面说过了。

另外线程池肯定是需要线程去执行具体的任务的,所以在线程池中就封装了一个内部类 Worker 作为工作线程,每个 Worker 中都维持着一个 Thread。

线程池的重点之一就是控制线程资源合理高效的使用,所以必须控制工作线程的个数,所以需要保存当前线程池中工作线程的个数。

看到这里,你是否觉得需要用两个变量来保存线程池的状态和线程池中工作线程的个数呢?但是在 ThreadPoolExecutor 中只用了一个 AtomicInteger 型的变量就保存了这两个属性的值,那就是 ctl

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;

ctl是线程池的核心状态控制字段,本身是一个AtomicInteger,用来保证对ctl的操作都是线程安全的。这里利用位运算巧妙地将一个int(一个int 4个字节 即32位)拆成了两部分,高3位用来表示线程的状态,剩下的29位则表示工作线程数。这里就可以得知工作线程的数量上限即CAPACITY,大约有5亿。

这五种状态转换成二进制后如下所示:

• RUNNING: 0b11100000_00000000_00000000_00000000

   能接受新提交的任务,并且也能处理阻塞队列中的任务

• SHUTDOWN: 0b00000000_00000000_00000000_00000000                         

关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。(finalize() 方法在执行过程中也会调用shutdown()方法进入该状态)

• STOP: 0b00100000_00000000_00000000_00000000

     不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态

• TIDYING: 0b01000000_00000000_00000000_00000000

如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态

 • TERMINATED: 0b01100000_00000000_00000000_00000000

    在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做。

这五种状态中,只有RUNNING的最高位是1,为负数,所以只需要判断ctl是否大于0就能得知线程是否处于该状态。

private static boolean isRunning(int c) {
    return c < SHUTDOWN;
}

至于为什么ThreadPoolExecutor要利用位运算用一个AtomicInteger来表示状态跟工作线程数量,是为了在多线程环境下保证运行状态与线程数量的统一,利用AtomicInteger的原子性操作保证一致性

ThreadPoolExecutor提供了两个方法通过位运算来判断状态与工作线程数

private static int runStateOf(int c) { return c & ~CAPACITY; }

private static int workerCountOf(int c) { return c & CAPACITY; }

3、工作流程

了解了线程池中所有的重要属性之后,现在我们需要来了解下线程池的工作流程了。

上图是一张线程池工作的精简图,实际的过程比这个要复杂的多,不过这些应该能够完全覆盖到线程池的整个工作流程了。

整个过程可以拆分成以下几个部分:

3.1、提交任务

当向线程池提交一个新的任务时,线程池有三种处理情况,分别是:创建一个工作线程来执行该任务、将任务加入阻塞队列、拒绝该任务。

提交任务的过程也可以拆分成以下几个部分:

  • 当工作线程数小于核心线程数时,直接创建新的核心工作线程

  • 当工作线程数不小于核心线程数时,就需要尝试将任务添加到阻塞队列中去

  • 如果能够加入成功,说明队列还没有满,那么需要做以下的二次验证来保证添加进去的任务能够成功被执行

  • 验证当前线程池的运行状态,如果是非RUNNING状态,则需要将任务从阻塞队列中移除,然后拒绝该任务

  • 验证当前线程池中的工作线程的个数,如果为0,则需要主动添加一个空工作线程来执行刚刚添加到阻塞队列中的任务

  • 如果加入失败,则说明队列已经满了,那么这时就需要创建新的“临时”工作线程来执行任务

  • 如果创建成功,则直接执行该任务

  • 如果创建失败,则说明工作线程数已经等于最大线程数了,则只能拒绝该任务了

整个过程可以用下面这张图来表示:

3.2、创建工作线程

创建工作线程需要做一系列的判断,需要确保当前线程池可以创建新的线程之后,才能创建。

首先,当线程池的状态是 SHUTDOWN 或者 STOP 时,则不能创建新的线程。

另外,当线程工厂创建线程失败时,也不能创建新的线程。

还有就是当前工作线程的数量与核心线程数、最大线程数进行比较,如果前者大于后者的话,也不允许创建。

除此之外,会尝试通过 CAS 来自增工作线程的个数,如果自增成功了,则会创建新的工作线程,即 Worker 对象。

然后加锁进行二次验证是否能够创建工作线程,最后如果创建成功,则会启动该工作线程。

3.3、启动工作线程

当工作线程创建成功后,也就是 Worker 对象已经创建好了,这时就需要启动该工作线程,让线程开始干活了,Worker 对象中关联着一个 Thread,所以要启动工作线程的话,只要通过 worker.thread.start() 来启动该线程即可。

启动完了之后,就会执行 Worker 对象的 run 方法,因为 Worker 实现了 Runnable 接口,所以本质上 Worker 也是一个线程。

通过线程 start 开启之后就会调用到 Runnable 的 run 方法,在 worker 对象的 run 方法中,调用了 runWorker(this) 方法,也就是把当前对象传递给了 runWorker 方法,让他来执行。

3.4、获取任务并执行

在 runWorker 方法被调用之后,就是执行具体的任务了,首先需要拿到一个可以执行的任务,而 Worker 对象中默认绑定了一个任务,如果该任务不为空的话,那么就是直接执行。

执行完了之后,就会去阻塞队列中获取任务来执行,而获取任务的过程,需要考虑当前工作线程的个数。

  • 如果工作线程数大于核心线程数,那么就需要通过 poll 来获取,因为这时需要对闲置的线程进行回收;

  • 如果工作线程数小于等于核心线程数,那么就可以通过 take 来获取了,因此这时所有的线程都是核心线程,不需要进行回收,前提是没有设置 allowCoreThreadTimeOut

4、线程池带来的问题

4.1、线程池引发的内存泄露

问题回顾

最近由于业务需求使用到了线程池对数据进行异步处理,上线后系统正常运行了两天多突然收到了一波Full GC的告警,赶紧dump了堆信息并回滚了服务。分析dump文件后发现了一个LinkedBlockingQueue类型的大对象,就想到是上次改的线程池的问题了,因为对线程池使用的不熟悉,导致了线上问题。当时错误的写法如下:

private ThreadPoolExecutor executorService = 
  new ThreadPoolExecutor(0, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQeque<>());

上述代码核心线程数设置为0,默认会创建一个线程进行任务的处理,但是BlockingQueue使用的是LinkedBlockingQeque是一个无界的队列。

public LinkedBlockingQeque() {
    this(Integer.MAX_VALUE);
}

当核心线程数满了的时候后续的任务会优先插入队列中,只有当队列满了才会在最大线程数的范围内新增线程,然而因为是无界队列,所以此时设置的最大线程数就无效了。

这个出问题的业务,由于需要异步执行的任务耗时比较久而且任务量较大,只有一个线程根本消费不完,队列就持续地在增长,最终使得BlockingQueue成了一个大对象导致频繁的Full GC。

解决方案

重新调整了线程池的核心线程数与最大线程数,并将无界队列改为了有界队列防止大对象的生成。

private ThreadPoolExecutor executorService = 
  new ThreadPoolExecutor(4, 16, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1024));

5、ThreadPoolExecutor执行流程分析

5.1、execute方法

ThreadPoolExecutor的顶级父类是Executor接口,它只有一个方法就是execute(),我们也就是通过它来向线程池提交任务去执行的。

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

通过源码分析可以得知,当向线程池提交任务时处理流程如下

  • 若当前线程数小于corePoolSize,则创建一个新的线程来执行任务
  • 若当前线程数大于等于corePoolSize,且阻塞队列未满,则将任务添加到队列中
  • 若当前线程数大于等于corePoolSize且小于maximumPoolSize,同时阻塞队列已满,则创建一个“临时”线程来执行任务
  • 若当前线程数大于等于maximumPoolSize,且阻塞队列已满,此时就会执行拒绝策略

这里有两点需要注意的是:

  • 1、在往队列中添加任务后会对线程池进行double check,这是因为在并发情况下,从上次判断线程池状态到现在线程池可能会被关闭,由于线程池关闭后不能再继续添加任务了,此时就需要回滚刚才的添加任务到队列中的操作并通过拒绝策略拒绝该任务
  • 2、addWorker(null, false),这个方法执行时只是创建了一个新的线程,但是没有传入任务,这是因为前面已经将任务添加到队列中了

5.2、getTask方法

getTask方法用于从阻塞队列中获取任务,源码如下

private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?

        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);

            // Are workers subject to culling?
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }

            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

getTask方法首先对线程池状态进行判断,如果线程池为非RUNNING状态且满足以下条件

1.rs >= STOP,线程池是否正在stop2.阻塞队列是否为空

则将workerCount减1并返回null,这是因为当线程池状态为SHUTDOWN或以上时,不允许再往队列中添加任务。

timed变量用来判断是否进行超时控制,allowCoreThreadTimeOut默认是false即核心线程不会因为超时被回收。

           try {
                    Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }

这部分代码为获取任务的核心逻辑,当timed为true时通过poll进行超时控制,如果在keepAliveTime时间内没有获取到任务,则返回null,否则会通过take方法阻塞获取任务。

5.3、ThreadPoolExecutor拓展方法

这里有两个方法的实现是空的

protected void beforeExecute(Thread t, Runnable r) { }
protected void afterExecute(Runnable r, Throwable t) { }

​​​​​​​主要是用于ThreadPoolExecutor的子类自己实现一些自定义功能,比如线程监控,ThreadPoolExecutor提供了以下方法可以用来监控线程状态

long getTaskCount()//线程池已经执行的和未执行的任务总数;
long getCompletedTaskCount()//线程池已完成的任务数量,该值小于等于taskCount;
int getLargestPoolSize()//线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过,也就是达到了maximumPoolSize;
int getPoolSize()//线程池当前的线程数量;
int getActiveCount()//当前线程池中正在执行任务的线程数量。

​​​​​​​

参考:

https://mp.weixin.qq.com/s/22e4s862E7tT45Ged019Eg

https://mp.weixin.qq.com/s/0KGNDELlrfhTmvzdUz0bIg

 

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值