ThreadPoolExecutor逻辑链梳理 - 2 拒绝处理机制

【从问题开始】

在使用线程池的时候往往会看到线程池reject相关日志,例如以下日志:

[Running, pool size = 80, active threads = 3, queued tasks = 3, completed tasks = 1674]

遂产生疑问:为什么线程池大小为80,队列长度为3的线程池,只有3个active threads就会触发reject机制?

 第一篇结尾处,根据execute方法的流程发散了两个问题:

  • RejectExecution的日志输出为什么会将alive threads输出为3?
  • 核心线程与非核心线程的添加流程是什么?为什么会添加失败?

 本篇就问题1探寻线程池的拒绝策略。

目录

线程池的拒绝处理机制 - RejectedExecutionHandle


线程池的拒绝处理机制 - RejectedExecutionHandler

线程池的构造函数中指定了线程池的拒绝处理机制:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         threadFactory, defaultHandler);
}

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), handler);
}

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;
}

根据线程池的构造函数可知,传入的handler被保存至成员属性中。

private volatile RejectedExecutionHandler handler;

如果不传,默认使用defaultHandler。

再看handler属性的调用点:

final void reject(Runnable command) {
    handler.rejectedExecution(command, this);
}

可知,在execute流程中,拒绝策略触发时,实际调用的就是RejectedExecutionHandler::rejectedExecution方法。

RjectedExecutionHandler是一个接口,只提供了一个接口方法:rejectedExecution方法。在ThreadPoolExecutor中有四个实现:

它们分别是:

CallerRunsPolicy

如果线程因队列阻塞无法执行,直接让调用线程执行这个任务

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
        r.run();
    }
}

AbortPolicy和RejectedExecutionException

如果被调用线程因队列阻塞无法执行任务,抛出RejectedExecutionException异常

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    throw new RejectedExecutionException("Task " + r.toString() +
                                         " rejected from " +
                                         e.toString());
}

当拒绝策略为AbortPolicy时,会抛出线程池拒绝异常。输出信息类似:

[Running, pool size = 80, active threads = 3, queued tasks = 3, completed tasks = 1674]

DiscardPolicy

处理不了,直接静默丢弃

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}

DiscardOldestPolicy

新来任务时塞不进去时,从队列头拉取任务丢弃,执行新来的任务,丢弃老的

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
        e.getQueue().poll();
        e.execute(r);
    }
}

其中,默认实现是AbortPolicy

private static final RejectedExecutionHandler defaultHandler =
    new AbortPolicy();

在本例中,代码环境使用的即默认策略,抛出异常。

看AbortPolicy的实现:

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    throw new RejectedExecutionException("Task " + r.toString() +
                                         " rejected from " +
                                         e.toString());
}

输出实际上是ThreadPoolExecutor重写的toString方法

public String toString() {
    long ncompleted;
    int nworkers, nactive;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        ncompleted = completedTaskCount;
        nactive = 0;
        nworkers = workers.size();
        for (Worker w : workers) {
            ncompleted += w.completedTasks;
            if (w.isLocked())
                // 这里可知nactive实际取的是加锁成功的worker数量,而非有一个算一个
                ++nactive;
        }
    } finally {
        mainLock.unlock();
    }
    int c = ctl.get();
    String rs = (runStateLessThan(c, SHUTDOWN) ? "Running" :
                 (runStateAtLeast(c, TERMINATED) ? "Terminated" :
                  "Shutting down"));
    return super.toString() +
        "[" + rs +
        ", pool size = " + nworkers +
        ", active threads = " + nactive +
        ", queued tasks = " + workQueue.size() +
        ", completed tasks = " + ncompleted +
        "]";
}

toString方法对Worker的处理过程通过ReentrantLock加锁,加锁成功后才做workers状态的统计。从toString方法中可以看出,输出的几个核心指标:

  • nworkers:线程池大小,取自成员变量workers的大小,这个大小既不是maxPoolSize也不是corePoolSize,而是实际创建出来的worker的数量。既然触发了reject,往往这个值是大于等于maxPoolSize的,正常应该不会比它小。
  • nactive:活跃线程数量,实际上是取的worker中处于isLocked状态的数量。
  • workQueue.zise():根workers的size不同,这个是线程池的阻塞队列长度,根据第一篇中分析的reject的触发场景,一般发现是满了,那就是因为阻塞队列满而触发reject,如果没满,那多半是因为线程的状态已经是非运行态了,例如Executors.newSingleThreadExecutor.execute()在没有任务执行时恰好触发GC后,可能输出阻塞队列不满就触发reject的日志。
  • ncompleted:所有worker中completedTaskCount属性之和。

这就可以解释问题:为什么active thread还不满就触发reject了。但是同样,今天的分析也引出了以下几个问题:

由此我们提出以下几个问题,作为后续分析的方向:

  1. Worker是什么?它的运行机制是什么样的?
  2. 对Worker信息的统计为什么要加锁?

本篇结论:

  1.  reject策略在ThreadPoolExecutor中有四种实现,其中默认实现是Abort Policy,即触发reject时,抛出RejectExecution。
  2. Abort Policy的错误信息实际是ThreadPoolExecutor中toString方法的输出信息,在输出内容中实际是对Worker的统计。而active thread实际代表了处于isLocked状态的worker数量。
  • 22
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值