图解线程池——清新脱俗的讲原理
网上介绍线程池的文章很多,质量好坏不一。能讲的很透彻的,确实不多。
本人能力有限,本文先从原理入手,讲清楚线程池是怎么运行的。
至于源码的分析,将单独写一篇(《线程池源码详解》)。
全文以 java 1.8 来说明。
一、示例代码
public static void main(String[] args) throws Exception {ExecutorService pool = Executors.newFixedThreadPool(2);for(int i =1; i <= 10; i++){int index = i;pool.execute(new Runnable() {@Overridepublic void run() {String currentThreadName = Thread.currentThread().getName();log.info("第{}次任务结束,执行者:{}", index, currentThreadName);}});}pool.shutdown();System.out.println("All thread is over");}
这个小例子中,执行了10次 run() 方法,但其实只起了两个线程。
这就是线程池的一个特点:节省资源
线程的创建与销毁是非常耗能的操作,若执行一个任务就起一个线程,那是消耗大了去了。
二、线程池的创建
示例中,Executors.newFixedThreadPool(2)
,最终是执行的下面这个方法(是ThreadPoolExecutor
这个类中的方法)
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;}
这里面有7个参数,等介绍源码的时候再详细说。这里只说涉及本本篇的三个:
corePoolSize
核心线程数maximumPoolSize
最大线程数workQueue
阻塞队列
顺便说一句,在线程池里,没有什么所谓的核心线程、非核心线程。
有些文章里说核心线程怎么怎么样
,非核心线程怎么怎么样
——瞎掰!
线程池创建出来的线程,地位是一样的,功能也是一样的。
阻塞队列,之前的博客有详细的讲解,这里就再展开。
线程的执行
execute(Runnable command) 这个方法是核心方法。
public void execute(Runnable command) {if (command == null)throw new NullPointerException();int c = ctl.get();if (workerCountOf(c) < corePoolSize) { // 1、工作线程数量小于核心线程数,创建线程if (addWorker(command, true))return;c = ctl.get();}if (isRunning(c) && workQueue.offer(command)) { // 2、将任务放入阻塞队列int recheck = ctl.get();if (! isRunning(recheck) && remove(command))reject(command);else if (workerCountOf(recheck) == 0)addWorker(null, false);}else if (!addWorker(command, false)) // 3、队满时,继续创建线程,直至工作线程数量达到最大值。reject(command); // 4、执行拒绝策略}
我写了4个注释,这是execute方法的执行逻辑。这里我举个通俗的例子,来说明。
假如病毒流行,感染的病人被领到医院看病。这里的病人相当于需要执行的任务。
医院里总共有 6 套超级防护服,医生需穿上防护服才可以给病人看病。
医院里拿出4套防护服供医生使用,留下两套备用。
这里的 6 相当于 maximumPoolSize 最大线程数,4 相当于corePoolSize
如果医生都在忙,那病人就被安置到大厅,排队看医生。这里的大厅相当于workQueue
上面是一个空图,现在来了一个病人,医生穿上防护服,给病人看病。
代码层面,就是执行 addWorker(command, true)
这一行。
之后又来俩病人。那就继续创建线程,同时呢,假设第一个来的病人,已经看完了,离开了诊疗室。
现在呢,假如一下子来了三个病人,
那先新上一个医生看病,第二、三个病人到大厅等着。
其实空闲的那个医生,会到大厅里叫病人的
顺便说一句:当未达到核心线程数时,就先创建线程,不管其它线程是不是闲着。
之后再来的病人,就进大厅等着。相当于执行这行代码 workQueue.offer(command)
。
顺便说一句:医生不会闲着,看完一个,就从大厅叫一个进来。
如果病人来的又多又快,四个医生都没闲着,大厅也满了,那就把备用的两个防护服给用上。
相当于执行这行代码 addWorker(command, false)
。
现在是六个医生全力工作,看完一个病人,就从大厅领进来一个病人处理。
如果即使是这样,病人来是源源不断的来医院,这时就进行拒绝策略。
相当于执行这行代码 reject(command)
。
循环取任务
addWorker(Runnable firstTask, boolean core)
这个方法是创建线程并会启动线程。
最终会调用 Thread 类中 start()
方法。JVM 会自在合适的时候,调用 线程的 run()
方法。
如果面试官问——JVM 怎么调用 run 方法的,机制是什么,合适的时候是什么时候
我不知道,已经脱离java源码的范畴了,你想知道,自己看hotSpot 的源码吧
在 ThreadPoolExecutor
类中,重写了 run()
方法
public void run() {runWorker(this);}
runWork() 这个方法,简化如下。
final void runWorker(Worker w) {Thread wt = Thread.currentThread();Runnable task = w.firstTask;w.firstTask = null;w.unlock(); try {while (task != null || (task = getTask()) != null) {w.lock();try {task.run();} finally {task = null;w.completedTasks++;w.unlock();}}} finally {processWorkerExit(w, completedAbruptly);}}
这个方法中, task.run() 会调用示例代码中 重写的那个run() 方法,也就是执行任务了。
仔细看,任务是从 getTask()
方法中取出来的,是循环执行,while 循环
前面例子中,医生看完一个病人,会从大厅里叫一个新病人。
当一个医生从大厅里叫病人,比如叫了两分钟,都没叫到。
那就认为没病人了,会考虑要不要脱了防护服,回去休息。
要不要撤的依据是,诊疗室里的医生有几个,即与 corePoolSize
比较
如果大于4个,那就可以回去消息,否则就在诊疗室待命。
也就是说,没有病人的时候,会蜕变成上图的样子,直到一个病人与没有了,四个医生在待命。
取任务的方法,代码简化如下
private Runnable getTask() {for (;;) { int wc = workerCountOf(c);boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;try {Runnable r = timed ?workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :workQueue.take();if (r != null)return r;} catch (InterruptedException retry) {}}}
这个方法本质是从队列中取任务,取出来后,在 runWork() 方法里执行。
那最忙的时候,是六个医生在工作,之后不忙了,缩减到4个,代码层面体现在哪里呢?
看 runWork()
方法,在finally里执行 processWorkerExit()
这个方法。
能走到这个方法,意味着大厅里没有病人了,可以会缩减医生。
简化后的代码如下:
private void processWorkerExit(Worker w, boolean completedAbruptly) {final ReentrantLock mainLock = this.mainLock;mainLock.lock();try {completedTaskCount += w.completedTasks;workers.remove(w); // 缩减线程} finally {mainLock.unlock();}tryTerminate();}
workers.remove(w)
这个就是将工作线程给剔除,GC回收。
线程关闭
在示例代码中,调用 shutdown()
方法,会关闭线程。代码如下,这个不用简化,很简洁
public void shutdown() {final ReentrantLock mainLock = this.mainLock;mainLock.lock();try {checkShutdownAccess(); // 检查有没有权限关闭线程池advanceRunState(SHUTDOWN); // 更改线程池的状态interruptIdleWorkers(); // 中断线程onShutdown(); // hook for ScheduledThreadPoolExecutor} finally {mainLock.unlock();}tryTerminate();}
等到介绍源码的时候,再细说如何优雅的关闭线程池。
总结
梳理下,本篇简单介绍了,线程池的创建、运行、关闭。
用画图的方式,介绍了,线程池工作原理,即 execute() 方法的四大步
- 接到任务,先创建工作线程
- 工作线程达到一定数量,把任务往阻塞队列放
- 阻塞队列放不下了,创建工作线程(不超过最大线程数)
- 如果前三步应付不过来,执行拒绝策略
线程工作,即 addWorker()
方法执行里,调用 Thread
类的 start()
方法,JVM 会调用 run()
方法
重写的 run()
方法执行 runWorker()
方法,此方法会循环取任务,直到没有任务了。
随后会维护线程池里线程的数量。最后是关闭线程池。
看了之后,有没有一种错觉,线程池的代码挺简单的呀,没有什么复杂的逻辑呀!
事实是代码巨复杂,这篇文章是把代码简化了,只说大流程。
有了这篇的基础,下篇解析源码时,会详细说明
- 线程池状态是怎么控制的
- 创建线程是怎么防止并发的
- 核心线程数和最大线程数,具体干啥的
- 哪些地方用了 AQS 框架
- 线程最长存活时间,是怎样控制线程生命周期的
- addWorker 时,传一个空任务,是干嘛的
- 常用线程池,是怎么选阻塞队列的
- ……
所有这些,请看下篇《线程池源码详解》。
图解线程池——清新脱俗的讲原理相关推荐
- 深入理解线程池(ThreadPoolExecutor)——细致入微的讲源码。
在上一篇博文<图解线程池原理>中,大体上介绍了线程池的工作原理. 这一篇从源码层面,细致剖析,文章会很长. 如果上篇文章内容没吸收,先看上篇,先易后难嘛. 本文源码是 java 1.8 版 ...
- Java线程池,从使用到原理
转载自 Java线程池,从使用到原理 线程池的技术背景 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源.在Java中更是如此,虚拟机将试图跟踪每一个对象 ...
- java线程池的工作原理_Java 线程池的介绍以及工作原理
在什么情况下使用线程池? 1.单个任务处理的时间比较短 2.将需处理的任务的数量大 使用线程池的好处: 1. 降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的消耗. 2. 提高响应速度 ...
- Java 线程池的介绍以及工作原理
在什么情况下使用线程池? 1.单个任务处理的时间比较短 2.将需处理的任务的数量大 使用线程池的好处: 1. 降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的消耗. 2. 提高响应速度 ...
- 线程池作用、用法以及原理
线程池 作用 用法 建议设定大小 快捷构造线程池 submit与execute shutdown与shutdownNow Future与FutureTast 代码 状态 底层原理 继承关系 主要参数 ...
- JUC多线程:线程池的创建及工作原理 和 Executor 框架
一.什么是线程池: 线程池主要是为了解决 新任务执行时,应用程序为任务创建一个新线程 以及 任务执行完毕时,销毁线程所带来的开销.通过线程池,可以在项目初始化时就创建一个线程集合,然后在需要执行新任务 ...
- java map集合 事务控制_对象回收过程?线程池执行过程? map原理?集合类关系?synchronized 和 volatile ? 同一个类的方法事务传播控制还有作用吗?java 锁...
1. 对象回收过程? 可达性分析算法: 如果一个对象从 GC Roots 不可达时,则证明此对象不可用. 通过一系列称为GC ROOTS的对象作为起点,从这些起点往下搜索,搜索走过的路径 称为引用链 ...
- 给女朋友讲 : Java线程池的内部原理
文章持续更新,微信搜索「 万猫学社 」第一时间阅读. 关注后回复「 电子书 」,免费获取12本Java必读技术书籍. 餐厅的约会 餐盘在灯光的照耀下格外晶莹洁白,女朋友拿起红酒杯轻轻地抿了一小口,对我 ...
- ScheduleThreadPoolExecutor 定时任务线程池原理
ScheduleThreadPoolExecutor 定时任务线程池 ScheduleThreadPoolExecutor 定时任务线程池 定时任务实现方法 实现原理 ScheduleFutureTa ...
最新文章
- 源码资本张宏江:只有算法和技术,那你一定挣不到钱
- fedora如何隐藏顶部状态栏_如何使用PDF Arranger来对PDF文件进行排版和修改
- Windows10 家庭版没有本地组策略解决方法
- flash 遮罩层全解
- Android持久化存储(2)SharedPreferences使用介绍
- 波特率、信息传输速率与带宽的关系
- 解决java报Too many open files错误
- DFS 下沙小面的(2)
- beetl html模板,Spring Boot 2 中使用 beetl 模板引擎
- cs229 机器学习
- html动画3d背景图片,jQuery和CSS3全屏3D背景图片视觉差特效
- 前端自学学习笔记——JavaScript
- jquery1.7版本核心模块测试封装
- ABAQUS均匀受力,边界条件对称,但结果云图不对称
- android每天定时打卡,钉钉定时打卡脚本下载|叉叉助手钉钉定时打卡插件安卓版下载 v4.3.1 - 跑跑车安卓网...
- 用 matplotlib 做交互式的票房分析
- html5动效系列二:超级惊艳 10款HTML5动画特效推荐
- Rubby在青柠互动的一月份工作总结
- 技术:双电脑共享鼠标、键盘解决方案 | USB对拷线、Synergy
- [常系数(非)齐次线性递推]
热门文章
- 重磅!解密央行数字货币研究所第二任所长穆长春(附完整个人简历)
- shell快捷键总结
- 2021年电工(中级)新版试题及电工(中级)实操考试视频
- 信用机制的发展与区块链的诞生
- js replace 全部替换
- Bypassing the Monster: A Faster and Simpler Optimal Algorithm for Contextual Bandits under Realizabi
- js关于indexOf的使用要注意的问题
- 书名: 岛上书店 简评:一个书店将各个人物的生活事业联系起来。
- 怎样把flv转换成mp4,4种方法轻松学
- 做闲鱼两年了,bug不少