最近在项目中遇到一个需要用线程池来处理任务的需求,于是我用ThreadPoolExecutor来实现,但是在实现过程中我发现提交大量任务时它的处理逻辑是这样的(提交任务还有一个submit方法内部也调用了execute方法):

public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
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);
}

注释中已经写的非常明白:

  1. 如果线程数量小于corePoolSize,直接创建新线程处理任务
  2. 如果线程数量等于corePoolSize,尝试将任务放到等待队列里
  3. 如果等待队列已满,尝试创建非核心线程处理任务(如果maximumPoolSIze > corePoolSize

但是在我的项目中一个线程启动需要10s左右的时间(需要启动一个浏览器对象),因此我希望实现一个更精细的逻辑提升资源的利用率:

  1. 线程池保持corePoolSize个线程确保有新任务到来时可以立即得到执行
  2. 当没有空闲线程时,先把任务放到等待队列中(因为开启一个线程需要10s,所以如果在等待队列比较小的时候,等待其他任务完成比等待新线程创建更快)
  3. 当等待队列的大小大于设定的阈值threshold时,说明堆积的任务已经太多了,这个时候开始创建非核心线程直到线程数量已经等于maximumPoolSize
  4. 当线程数量已经等于maximumPoolSize,再将新来的任务放回到任务队列中等待(直到队列满后开始拒绝任务)
  5. 长时间空闲后退出非核心线程回收浏览器占用的内存资源

当我研究了常见的CachedThreadPoolFixedThreadPool以及尝试自己配置ThreadPoolExecutor的构造函数后,发现无论如何都不能实现上面提到的逻辑,因为默认的实现只有在workQueue达到容量上限后才会开始创建非核心线程,因此需要通过继承的方法实现一个新的类来完成需求。

怎么实现在workQueue到达容量上限前就创建非核心线程?还要回顾下execute函数的代码

					//尝试将任务插入等待队列,如果返回false
//说明队列已经到达容量上限,进入else if逻辑
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);

那么只要改变workQueue.offer()的逻辑,在线程数量还小于maximumPoolSize的时候就返回false拒绝插入,让线程池调用addWoker,等不能再创建更多线程时再允许添加到队列即可。

可以通过子类重写offer方法来实现添加逻辑的改变

@Override
public boolean offer(E e) {
if (threadPoolExecutor == null) {
throw new NullPointerException();
}
//当调用该方法时,已经确定了workerCountOf(c) > corePoolSize
//当数量小于threshold,在队列里等待
if (size() < threshold) {
return super.offer(e);
//当数量大于等于threshold,说明堆积的任务太多,返回false
//让线程池来创建新线程处理
} else {
//此处可能会因为多线程导致错误的拒绝
if (threadPoolExecutor.getPoolSize() < threadPoolExecutor.getMaximumPoolSize()) {
return false;
//线程池中的线程数量已经到达上限,只能添加到任务队列中
} else {
return super.offer(e);
}
}
}

这样就实现了基本实现了我需要的功能,但是在写代码的过程中我找到了一个可能出错的地方:ThreadPoolExecutor线程安全的,那么重写的offer方法也可能遇到多线程调用的情况

//设想当poolSize = maximumPoolSize-1时,两个任务到达此处同时返回false
if (threadPoolExecutor.getPoolSize() < threadPoolExecutor.getMaximumPoolSize()) {
return false;
}

由于添加到队列返回falseexecute方法进入到else if (!addWorker(command, false))

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);
}
//添加到队列失败后进入addWorker方法中
else if (!addWorker(command, false))
reject(command);
}

再来看一下addWorker方法的代码,这里只截取需要的一部分

for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
//两个线程都认为还可以创建再创建一个新线程
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//两个线程同时调用cas方法只有一个能够成功
//成功的线程break retry;进入后面的创建线程的逻辑
//失败的线程重新回到上面的检查并返回false
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}

最终,在竞争中失败的线程由于addWorker方法返回了false最终调用了reject(command)。在前面写的要实现的逻辑里提到了,只有在等待队列容量达到上限无法再插入时才拒绝任务,但是由于多线程的原因,这里只是超过了threshold但没有超过capacity的时候就拒绝任务了,所以要对拒绝策略的触发做出修改:第一次触发Reject时,尝试重新添加到任务队列中(不进行poolSize的检测),如果仍然不能添加,再拒绝任务

这里通过对execute方法进行重写来实现重试

@Override
public void execute(Runnable command) {
try {
super.execute(command);
} catch (RejectedExecutionException e) {
/*
这里参考源码中将任务添加到任务队列的实现
但是其中通过(workerCountOf(recheck) == 0)
检查当任务添加到队列后是否还有线程存活的部分
由于是private权限的,无法实现类似的逻辑,因此需要做一定的特殊处理
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);
}
*/
if (!this.isShutdown() && ((MyLinkedBlockingQueue)this.getQueue()).offerWithoutCheck(command)) {
if (this.isShutdown() && remove(command))
//二次检查
realRejectedExecutionHandler.rejectedExecution(command, this);
} else {
//插入失败,队列已经满了
realRejectedExecutionHandler.rejectedExecution(command, this);
}
}
}
}

这里有两个小问题:

  1. 初始化线程池传入的RejectedExecutionHandler不一定会抛出异常(事实上,ThreadPoolExecutor自己实现的4中拒绝策略中只有AbortPolicy能够抛出异常并被捕捉到),因此需要在初始化父类时传入AbortPolicy拒绝策略并将构造函数中传入的自定义拒绝策略保存下来,在重试失败后才调用自己的rejectedExecution
  2. corePoolSize = 0 的极端情况下,可能出现一个任务刚被插入队列的同时,所有的线程都结束任务然后被销毁了,此使这个被加入的任务就无法被执行,在ThreadPoolExecutor中是通过
    else if (workerCountOf(recheck) == 0)
    addWorker(null, false);

    在添加后再检查工作线程是否为0来确保任务可以被执行,但是其中使用的方法是私有的,无法在子类中实现类似的逻辑,因此在初始化时只能强制corePoolSize至少为1来解决这个问题。

全部代码如下

public class MyThreadPool extends ThreadPoolExecutor {

    private RejectedExecutionHandler realRejectedExecutionHandler;

    public MyThreadPool(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
int queueCapacity) {
this(corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
queueCapacity,
new AbortPolicy());
} public MyThreadPool(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
int queueCapacity,
RejectedExecutionHandler handler) {
super(corePoolSize == 0 ? 1 : corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
new MyLinkedBlockingQueue<>(queueCapacity),
new AbortPolicy());
((MyLinkedBlockingQueue)this.getQueue()).setThreadPoolExecutor(this);
realRejectedExecutionHandler = handler;
} @Override
public void execute(Runnable command) {
try {
super.execute(command);
} catch (RejectedExecutionException e) {
if (!this.isShutdown() && ((MyLinkedBlockingQueue)this.getQueue()).offerWithoutCheck(command)) {
if (this.isShutdown() && remove(command))
//二次检查
realRejectedExecutionHandler.rejectedExecution(command, this);
} else {
//插入失败,队列已经满了
realRejectedExecutionHandler.rejectedExecution(command, this);
}
}
}
} public class MyLinkedBlockingQueue<E> extends LinkedBlockingQueue<E> { private int threshold = 20; private ThreadPoolExecutor threadPoolExecutor = null; public MyLinkedBlockingQueue(int queueCapacity) {
super(queueCapacity);
} public void setThreadPoolExecutor(ThreadPoolExecutor threadPoolExecutor) {
this.threadPoolExecutor = threadPoolExecutor;
} @Override
public boolean offer(E e) {
if (threadPoolExecutor == null) {
throw new NullPointerException();
}
//当调用该方法时,已经确定了workerCountOf(c) > corePoolSize
//当数量小于threshold,在队列里等待
if (size() < threshold) {
return super.offer(e);
//当数量大于等于threshold,说明堆积的任务太多,返回false
//让线程池来创建新线程处理
} else {
//此处可能会因为多线程导致错误的拒绝
if (threadPoolExecutor.getPoolSize() < threadPoolExecutor.getMaximumPoolSize()) {
return false;
//线程池中的线程数量已经到达上限,只能添加到任务队列中
} else {
return super.offer(e);
}
}
} public boolean offerWithoutCheck(E e) {
return super.offer(e);
}
}

最后进行简单的测试

corePoolSize:2
maximumPoolSize:5
queueCapacity:10
threshold:7
任务2
线程数量:2
等待队列大小:0
等待队列大小小于阈值,继续等待。
任务3
线程数量:2
等待队列大小:1
等待队列大小小于阈值,继续等待。
任务4
线程数量:2
等待队列大小:2
等待队列大小小于阈值,继续等待。
任务5
线程数量:2
等待队列大小:3
等待队列大小小于阈值,继续等待。
任务6
线程数量:2
等待队列大小:4
等待队列大小小于阈值,继续等待。
任务7
线程数量:2
等待队列大小:5
等待队列大小小于阈值,继续等待。
任务8
线程数量:2
等待队列大小:6
等待队列大小小于阈值,继续等待。
任务9
线程数量:2
等待队列大小:7
等待队列大小大于等于阈值,线程数量小于MaximumPoolSize,创建新线程处理。
任务10
线程数量:3
等待队列大小:7
等待队列大小大于等于阈值,线程数量小于MaximumPoolSize,创建新线程处理。
任务11
线程数量:4
等待队列大小:7
等待队列大小大于等于阈值,线程数量小于MaximumPoolSize,创建新线程处理。
任务12
线程数量:5
等待队列大小:7
等待队列大小大于等于阈值,但线程数量大于等于MaximumPoolSize,只能添加到队列中。
任务13
线程数量:5
等待队列大小:8
等待队列大小大于等于阈值,但线程数量大于等于MaximumPoolSize,只能添加到队列中。
任务14
线程数量:5
等待队列大小:9
等待队列大小大于等于阈值,但线程数量大于等于MaximumPoolSize,只能添加到队列中。
任务15
线程数量:5
等待队列大小:10
等待队列大小大于等于阈值,但线程数量大于等于MaximumPoolSize,只能添加到队列中。
队列已满
任务16
线程数量:5
等待队列大小:10
等待队列大小大于等于阈值,但线程数量大于等于MaximumPoolSize,只能添加到队列中。
队列已满

再重新复习一遍要实现的功能:

  1. 线程池保持corePoolSize个线程确保有新任务到来时可以立即得到执行
  2. 当没有空闲线程时,先把任务放到等待队列中(因为开启一个线程需要10s,所以如果在等待队列比较小的时候,等待其他任务完成比等待新线程创建更快)
  3. 当等待队列的大小大于设定的阈值threshold时,说明堆积的任务已经太多了,这个时候开始创建非核心线程直到线程数量已经等于maximumPoolSize
  4. 当线程数量已经等于maximumPoolSize,再将新来的任务放回到任务队列中等待(直到队列满后开始拒绝任务)
  5. 长时间空闲后退出非核心线程回收浏览器占用的内存资源

可以看出,线程池运行的逻辑和要实现的目标是相同的。

如何让ThreadPoolExecutor更早地创建非核心线程的更多相关文章

  1. JS 究竟是先有鸡还是有蛋,Object与Function究竟谁出现的更早,Function算不算Function的实例等问题杂谈

    壹 ❀ 引 我在JS 疫情宅在家,学习不能停,七千字长文助你彻底弄懂原型与原型链一文中介绍了JavaScript原型与原型链,以及衍生的__proto__.constructor等一系列属性.在解答了 ...

  2. Xcode7使用插件的简单方法&&以及怎样下载到更早版本的Xcode

    Xcode7自2015年9上架以来也有段时间了, 使用Xcode7以及Xcode7.1\Xcode7.2的小伙伴会发现像VVDocumenter-Xcode\KSImageNamed-Xcode\HO ...

  3. [think]关于个人发展值得记住的一些建议 听别人的话,即使你不想听 不要只做不想 成功不能被复制,但失败总在不停复制。看看别人是怎么倒下的,你可以更早地成功

    [think]关于个人发展值得记住的一些建议 偶然看到一篇采访周爱民的文章,里面的一些建议虽然朴实无华,却感觉很有道理,特此记录: 记者:对于程序员的技术发展和职业规划能否给大家一些建议呢?----- ...

  4. windows多线程(十一) 更安全的创建线程方式_beginthreadex()

    一.原因分析 CreateThread()函数是Windows提供的API接口,在C/C++语言另有一个创建线程的函数_beginthreadex(),我们应该尽量使用_beginthreadex() ...

  5. 更好的使用JAVA线程池

    这篇文章分别从线程池大小参数的设置.工作线程的创建.空闲线程的回收.阻塞队列的使用.任务拒绝策略.线程池Hook等方面来了解线程池的使用,其中涉及到一些细节包括不同参数.不同队列.不同拒绝策略的选择. ...

  6. 如何更好的使用JAVA线程池

    这篇文章结合Doug Lea大神在JDK1.5提供的JCU包,分别从线程池大小参数的设置.工作线程的创建.空闲线程的回收.阻塞队列的使用.任务拒绝策略.线程池Hook等方面来了解线程池的使用,其中涉及 ...

  7. java线程池及创建多少线程合适

    java线程池 1.以下是ThreadPoolExecutor参数完备构造方法: public ThreadPoolExecutor(int corePoolSize,int maximumPoolS ...

  8. [转]使用VC/MFC创建一个线程池

    许多应用程序创建的线程花费了大量时间在睡眠状态来等待事件的发生.还有一些线程进入睡眠状态后定期被唤醒以轮询工作方式来改变或者更新状态信息.线程池可以让你更有效地使用线程,它为你的应用程序提供一个由系统 ...

  9. Java多线程01(Thread类、线程创建、线程池)

    Java多线程(Thread类.线程创建.线程池) 第一章 多线程 1.1 多线程介绍 1.1.1 基本概念 进程:进程指正在运行的程序.确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于 ...

随机推荐

  1. return console.log()结果为undefined现象的解答

    console.log总是出现undefined--麻烦的console //本文为作者自己思考后总结出的一些理论知识,若有错误,欢迎指出 bug出现 ​ 需求如下:新建一个car对象,调用其中的de ...

  2. turtle实例

    1.彩虹 (1) from turtle import * def HSB2RGB(hues): hues = hues * 3.59 #100转成359范围 rgb=[0.0,0.0,0.0] i ...

  3. CSS超链接样式,去除下划线等

    控制超链接样式 链接的四种状态: a:link - 普通的.未被访问的链接 a:visited - 用户已访问的链接 a:hover - 鼠标指针位于链接的上方 a:active - 链接被点击的时刻 ...

  4. 【公告】请访问我Blog新站——superman2014 www.superman2014.com

    http://www.superman2014.com 欢迎光顾 本博客不在更新!!!!

  5. 三通道低功耗ASK低频唤醒接收器PAN3501完全替代AS3933/GC3933

    低频唤醒接收器PAN3501软硬件兼容AS3933/GC3933,且新增了寄存器功能,可直接替换,供应稳定,高性价比. 产品介绍:    PAN3501是一款最多三个通道接收的低功耗ASK接收机,可用 ...

  6. Java中如何通过try优雅地释放资源?

    时间紧迫,长话短说,今天,小明给大家同步一个知识点,使用try-with-resources来优雅地关闭资源. 1. 背景 其实,在JDK 7就已经引入了对try-with-resources的支持, ...

  7. Jmeter 压力测试笔记(2)--问题定位

    事情已经出了,是该想办法解决的时候了. 经过运维和DBA定位: 数据库读写分离中,读库延时超过了30秒,导致所有请求都压在主库.另外所有数据库都连接数都被占满,但活跃请求数量缺不多. 数据库16K的连 ...

  8. ansible--ansible基础

    配置文件 ansible的配置文件只有一个,即ansible.cfg,它可以存在于多个地方,ansible读取配置文件的顺序依次是当前命令执行目录->用户家目录下的.ansible.cfg-&g ...

  9. 这个案例写出来,还怕跟面试官扯不明白 OAuth2 登录流程?

    昨天和小伙伴们介绍了 OAuth2 的基本概念,在讲解 Spring Cloud Security OAuth2 之前,我还是先来通过实际代码来和小伙伴们把 OAuth2 中的各个授权模式走一遍,今天 ...

  10. 一天学一个Linux命令:第二天 cd pwd

    文章更新于:2020-03-08 注:本文参照 man pwd 手册,并给出使用样例. 文章目录 一.命令之 `cd` 和 `pwd` 1.命令介绍 2.语法格式 3.使用样例 4.pwd 参数 5. ...