最近在项目中遇到一个需要用线程池来处理任务的需求,于是我用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. 用pymysql和Flask搭建后端,响应前端POST和GET请求

    前言 这次作业不仅需要我建立一个数据库(详情请点击这里),还需要我基于这个数据库写后端接口(注册和登录)供前端访问,接收前端的POST和GET请求,并将登录.注册是否成功传给前端. 本文介绍如何用Fl ...

  2. Python面向对象之异常处理

    1:什么是异常 异常就是在我们的程序在运行过程中由于某种错误而引发Python抛出的错误: 异常就是程序运行时发生错误的信号(在程序出现错误时,则会产生一个异常,若程序没有处理它,则会抛出该异常,程序 ...

  3. A 密码锁

    时间限制 : - MS   空间限制 : - KB  评测说明 : 1s,128m 问题描述 何老板有一把奇特的密码锁.密码锁上有n个数字(范围0到9)排成一排.密码锁上有两个按钮:每按一次1号按钮, ...

  4. C - Can you solve this equation? HDU - 2199(二分水题)

    Now,given the equation 8x^4 + 7x^3 + 2x^2 + 3x + 6 == Y,can you find its solution between 0 and 100; ...

  5. C - 饭卡

    电子科大本部食堂的饭卡有一种很诡异的设计,即在购买之前判断余额.如果购买一个商品之前,卡上的剩余金额大于或等于5元,就一定可以购买成功(即使购买后卡上余额为负),否则无法购买(即使金额足够).所以大家 ...

  6. M - 湫湫系列故事——减肥记I

    M - 湫湫系列故事--减肥记I 对于吃货来说,过年最幸福的事就是吃了,没有之一! 但是对于女生来说,卡路里(热量)是天敌啊! 资深美女湫湫深谙"胖来如山倒,胖去如抽丝"的道理,所 ...

  7. JSP+SSH+Mysql+C3P0实现的传智播客网上商城

    项目简介 项目来源于:https://gitee.com/2121/shop 本系统是传智播客授课时的开发案例,基于JSP+SSH+Mysql的简单网上商城.在当代开发中,SSH的使用已经逐渐被SSM ...

  8. 剖析手写Vue,你也可以手写一个MVVM框架

    剖析手写Vue,你也可以手写一个MVVM框架# 邮箱:563995050@qq.com github: https://github.com/xiaoqiuxiong 作者:肖秋雄(eddy) 温馨提 ...

  9. Jenkins中管道案例脚本(生命式语法)

    pipeline { agent any parameters { choice( choices: 'feature\nmaster\npercent10', description: '选择要发布 ...

  10. 计算机网络篇,基于UDP、TCP的应用层及其端口