java ThreadPoolExecutor初探
导读:线程池是开发中使用频率比较高的组件之一,但是又有多少人真正了解其内部机制呢。
关键词:线程池
前言
线程池是大家开发过程中使用频率比较高的组件之一,但是其内部原理又有多少人真正清楚呢。最近抽时间去了解了一下其内部实现细节,感觉略有收获,遂以ThreadPoolExecuter为例将自己的心得体会分享出来和大家一起交流,如有不妥之处,烦请大家积极指正。
我的疑问
- 执行任务的工作机制是怎样的?
- 是什么时机清理空闲线程的?
- ThreadPoolExecutor.CallerRunsPolicy 什么场景下会用到?
- 线程池终止的时候都做了什么?(过两天补上)
问题1-执行任务的工作机制是怎样的?
开始之前,我先试图用一个真实的场景来描述一下,2015年我们公司boss开始创立公司,创立初期公司业务比较少,他一个人(corePoolSize=1)干的有条不紊,有声有色的,没过多久,业务量上来了,他一个人干不过来,分身乏力,那怎么办呢?其实很简单,排队呗,就这样boss将待办的任务都添加到备忘录(BlockIngQueue)里面,boss又开始愉快的工作,但是客户的耐心终归有限,过了几天发现自己交给我们公司的业务还没完成,客户一气之下打电话给boss“我的活你干完没有,没干的话就停下来(shutdown/shutdownNow)吧,我找别人了”,这时候boss慌了,默默的点上一根烟,在网上发了招聘通知,就这样干活的人又多了起来(addWorker),公司在boss的带领下风生水起,就这样不知不觉中公司走过了五个年头,本以为可以大干一场的我们,却偏偏赶上了2020年的新冠,复工日期一拖再拖,客户需求一少再少,唯独公司养的员工没少,这是公司目前最大的开支了。长痛不如短痛,boss下发了一个政策,如果员工本月(keepAliveTime=一个月)kpi完不成,假定kpi为“单月完成任务数大于0“,那么就会被淘汰(空闲线程被清理),最后撑了两个月,公司又回到解放前,boss又成了光杆司令。
文字描述
提交任务步骤1:启动核心工作线程
触发条件:工作线程数小于核心线程数
步骤描述:启动核心工作线程,并将任务作为工作线程的firstTask,如果启动失败会走到“提交任务步骤2”
提交任务步骤2:往阻塞队列中堆积任务
触发条件:工作线程数大于核心线程数
步骤描述:尝试将任务堆积到队列中,如果堆积失败,会走到“提交任务步骤3”。如果堆积成功会做两个双重检查,分别是状态的检查和工作线程数的检查,如果状态不合法就尝试删除刚添加成功的任务,删除成功调用reject方法(为什么删除失败不需要调用reject方法呢?因为删除失败意味着task已经被处理过了,不能谎报军情);如果工作线程数等于0就补充工作线程,什么情况下工作线程会变成0呢,后面会单独说这个问题
提交任务步骤3:启动工作线程
触发条件:核心线程数已达到线程池设定的阈值corePoolSize而且队列里已经堆积不了了
步骤描述:尝试启动工作线程,如果这时候启动失败就调用reject方法通知调用者
时序图描述

问题2-是什么时机清理空闲线程的?
我们先来回顾下线程的运行状态,如下图:

回到我们的问题,只需要让“空闲线程真的空闲下来”它自然就被清理了,那怎么能让它真的闲下来呢。前面时序图的第5步我提到worker会一直循环从BlockingQueue里面获取task执行,如果没有task返回给worker,那说明它真的是闲了(getTask返回null,意味着会退出while循环,很快worker的run方法就执行结束,线程从RUNNABLE到TERMINATED),它需要自己退出历史的长河中了,纵使它曾经立下汗马功劳,但是谁让它不是核心人员呢(你处于corePoolSize之外),让我们读一下getTask的代码:
private Runnable getTask() {
//标记位,获取任务是否超时
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int wc = workerCountOf(c);//worker的数量
// 标记位,可以简单理解为是否允许超时回收,允许的条件为allowCoreThreadTimeOut(是否允许核心线程被回收)或者worker的数量大于核心线程数
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
//满足以下几个条件时会返回null,1.worker的数量大于最大线程池的数量;
2.允许超时回收(timed==true)&&上一次获取任务超时(timedOut==true)&&当前没有堆积任务(workQueue.isEmpty),说明worker确实空闲了
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
//如果允许超时回收就使用poll,否则使用take
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
//poll结束,队列中当时没有任务,将timedOut置为true,下一趟循环时会满足回收的第二个条件,getTask返回null
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
问题3-ThreadPoolExecutor.CallerRunsPolicy 什么场景下会用到?
前面我们提到,如果线程池的处理能力已经饱和,那么就会调用reject方法执行RejectedExecutionHandler,jdk已经为我们提供了四种拒绝策略,分别是AbortPolicy,DiscardOldestPolicy,DiscardPolicy,CallerRunsPolicy,前三种都好理解,简单来说就是丢弃,唯独最后一种我想了好久没想到使用场景,前不久在看同事代码的时候发现他有用到这个策略,来达到一种限流的效果,简单画个流程图说明一下逻辑:
DelayQueuePollingTask负责定时从数据库拉取延迟队列,然后封装成task扔到线程池去执行,task使用httpclient向isv推送数据,由于isv的服务吞吐量比较低,经常会触发超时,进而导致线程池被跑满,为了让DelayQueuePollingTask可以感知到线程池处于饱和状态,而且又不至于它空等待,所以使用了CallerRunsPolicy这个策略,当线程池满负荷的时候由DelayQueuePollingTask所在的线程负责执行task。

总结
上周末带着自己之前使用线程池过程中的一些疑问学习了一下ThreadPoolExecutor源码,虽说没有完全吃透,但也带给自己不少收获,在这里将自己的思考过程分享出来,但愿能帮到一部分人。
如果觉得有用,请点个推荐。
java ThreadPoolExecutor初探的更多相关文章
- java并发初探ThreadPoolExecutor拒绝策略
java并发初探ThreadPoolExecutor拒绝策略 ThreadPoolExecuter构造器 corePoolSize是核心线程池,就是常驻线程池数量: maximumPoolSize是最 ...
- java并发初探ConcurrentSkipListMap
java并发初探ConcurrentSkipListMap ConcurrentSkipListMap以调表这种数据结构以空间换时间获得效率,通过volatile和CAS操作保证线程安全,而且它保证了 ...
- java并发初探ConcurrentHashMap
java并发初探ConcurrentHashMap Doug Lea在java并发上创造了不可磨灭的功劳,ConcurrentHashMap体现这位大师的非凡能力. 1.8中ConcurrentHas ...
- java并发初探CyclicBarrier
java并发初探CyclicBarrier CyclicBarrier的作用 CyclicBarrier,"循环屏障"的作用就是一系列的线程等待直至达到屏障的"瓶颈点&q ...
- java并发初探CountDownLatch
java并发初探CountDownLatch CountDownLatch是同步工具类能够允许一个或者多个线程等待直到其他线程完成操作. 当前前程A调用CountDownLatch的await方法进入 ...
- java并发初探ReentrantWriteReadLock
java并发初探ReentrantWriteReadLock ReenWriteReadLock类的优秀博客 ReentrantReadWriteLock读写锁详解 Java多线程系列--" ...
- Java内部类初探
Java内部类初探 之前对内部类的概念不太清晰,在此对内部类与外部类之间的关系以及它们之间的调用方式进行一个总结. Java内部类一般可以分为以下三种: 成员内部类 静态内部类 匿名内部类 一.成员内 ...
- JAVA ThreadPoolExecutor(转)
原文链接:http://blog.csdn.net/historyasamirror/article/details/5961368 基础 在我看来,java比C++的一个大好处就是提供了对多线程的支 ...
- 浅谈JAVA ThreadPoolExecutor(转)
这篇文章分为两部分,前面是ThreadPoolExecutor的一些基本知识,后一部分则是Mina中一个特殊的ThreadPoolExecutor代码解析.算是我的Java学习笔记吧. 基础 在我看来 ...
随机推荐
- Opencv笔记(五)——把鼠标当画笔
学习目标: 学习使用 OpenCV 处理鼠标事件 学会使用函数cv2.setMouseCallback() 简单演示: 首先我们来创建一个鼠标事件回调函数,但鼠标事件发生是他就会被执 ...
- 如何使用jQuery给asp.net的TextBox取值和赋值
解决办法: 可以在控件中先设置属性ClientInstandName的值和ID的值一样,再使用$("#ID").val("12345")
- Seikimatsu Occult Tonneru(网络流,状态数(建不建边)不多时,可考虑直接进行枚举
http://acm.hdu.edu.cn/showproblem.php?pid=4309 总结:边可存东西时,可新建一个点x连接u.v,x再连向汇点: #include<iostream&g ...
- HashMap相关知识
HashMap的工作原理是近年来常见的Java面试题.几乎每个Java程序员都知道HashMap,都知道哪里要用HashMap,知道Hashtable和HashMap之间的区别,那么为何这道面试题如此 ...
- The 2019 Asia Nanchang First Round Online Programming Contest C(cf原题,线段树维护矩阵)
题:https://nanti.jisuanke.com/t/41350 分析:先将字符串转置过来 状态转移,因为只有5个状态,所以 i 状态到 j 状态的最小代价就枚举[i][k]->[k][ ...
- RHCSA考试(Linux7)
博主本人平和谦逊,热爱学习,读者阅读过程中发现错误的地方,请帮忙指出,感激不尽 一.设置环境: 请初始化您的考试虚拟机 server0.example.com,将系统的 root 账号密码设置为 12 ...
- python与模块的导入方式
今日所得 模块 import from...import... 循环导入 相对导入 绝对导入 软件开发目录规范 模块 模块:是一系列功能的集合体 模块的三种来源:1.内置模块(Python解释器自带的 ...
- getline的使用
函数定义: getline(istream &in, string &s) 作用: 在C++中用 string 类型进行终端输入字符串时,解决无法输入带有空格的字符串的问题. 功能: ...
- FFT(快速傅里叶变换) 模板
洛谷 P3803 [模板]多项式乘法(FFT)传送门 存个板子,完全弄懂之后找机会再写个详解. #include<cstdio> #include<cmath> struct ...
- hadoop datanode 启动正常,但master无法识别(50030不显示datanode节点)
start-all.sh 启动 坑爹 找不出错 试了各种办法,重新formaet 查看 集群ID是否相同.都无效 日志也没看到错 按官网方法手动一步步启,问题照旧 master节点,yarn name ...