前言

在上一篇文章中我们介绍了线程池的使用,那么现在我们有个疑问:线程池到底是怎么实现的?毕竟好奇是人类的天性。那我们今天就来看看吧,扒开 他的源码,一探究竟。

1. 从 Demo 入手

上图是个最简单的demo,我们从这个 demo 开始看源码,首先一步一步来看。

首先我们手动创建了线程池,使用了有数量限制的阻塞队列,使用了线程池工厂提供的默认线程工厂,和一个默认的拒绝策略,我们看看默认的线程工厂是如何创建的?

默认的线程工厂从当前线程中获取线程组,设置了默认的线程名字前缀 pool-xxx-thread-xxx,强制设置为非守护线程,强制设置为默认优先级。

然后我们看看ThreadPoolExecutor 的构造方法:

没有什么特殊的东西,主要是一些判断。

好了,那么我们看看 execute 方法是如何实现的。

public void execute(Runnable command) {if (command == null)throw new NullPointerException();// c = -536870911int c = ctl.get();//  工作线程数量小于核心线程池设定数,则创建线程。if (workerCountOf(c) < corePoolSize) {// 如果添加成功则直接返回if (addWorker(command, true))return;// 否则再次获取活动线程数量c = ctl.get();}// 如果线程池正在运行,并且添加进队列成功if (isRunning(c) && workQueue.offer(command)) {// 再次对线程池状态检查, 因为上面 addWorker 过了并且失败了,所以需要检查int recheck = ctl.get();// 如果状态不是运行状态,且从队列删除该任务成功并尝试停止线程池if (! isRunning(recheck) && remove(command))// 拒绝任务reject(command);// 如果当前工作线程数量为0(线程池已关闭),则添加一个 null 到队列中else if (workerCountOf(recheck) == 0)// 添加个空的任务addWorker(null, false);}// 如果添加队列失败,则创建一个任务线程,如果失败,则拒绝else if (!addWorker(command, false))// 拒绝reject(command);}
}
复制代码

首先,空判断。

然后判断,如果正在工作的线程小于设置的核心线程,则创建线程并返回,如果正在工作的线程数量大于等于核心线程数量,则试图将任务放入队列,如果失败,则尝试创建一个 maximumPoolSize 的任务。注意,在remove 方法中,该方法已经试图停止线程池的运行。

从这段代码中,可以看到,最重要的方法就是 addWorker 和 workQueue.offer(command) 这段代码,一个是创建线程,一个是放入队列。后者就是将任务添加到阻塞队列中。

那么我们就看看 addWorker 方法。

2. addWorker 方法-----创建线程池

private boolean addWorker(Runnable firstTask, boolean core)

该方法很长,楼主说一下这个方法的两个参数,第一个参数为 Runnable 类型,表示线程池中某个线程的第一个任务,第二个参数是如果是 true,则创建 core 核心线程,如果是 false ,则创建 maximumPoolSize 线程。这两个线程的生命周期是不同的。

楼主截取该方法中最终的代码:

其中,在该方法中,创建一个 Worker 对象,该对象代理了任务对象,我们看看该类的构造方法:

通过线程工厂创建线程,注意,传递的是 this ,因此,在上面的代码中国,调用了 worker 对象的 thread 属性的 start 方法,实际上就是调用了该类的 run 方法。那么改类的 run 方法是怎么实现的呢?

调用了自身的 runWorker 方法。这个方法非常的重要。

3. Worker.runWorker(Worker w) 方法-------线程池的最核心方法

final void runWorker(Worker w) {Thread wt = Thread.currentThread();Runnable task = w.firstTask;w.firstTask = null;w.unlock(); // allow interruptsboolean completedAbruptly = true;try {while (task != null || (task = getTask()) != null) {w.lock();// If pool is stopping, ensure thread is interrupted;// if not, ensure thread is not interrupted.  This// requires a recheck in second case to deal with// shutdownNow race while clearing interruptif ((runStateAtLeast(ctl.get(), STOP) ||(Thread.interrupted() &&runStateAtLeast(ctl.get(), STOP))) &&!wt.isInterrupted())wt.interrupt();try {beforeExecute(wt, task);Throwable thrown = null;try {task.run();} catch (RuntimeException x) {thrown = x; throw x;} catch (Error x) {thrown = x; throw x;} catch (Throwable x) {thrown = x; throw new Error(x);} finally {afterExecute(task, thrown);}} finally {task = null;w.completedTasks++;w.unlock();}}completedAbruptly = false;} finally {processWorkerExit(w, completedAbruptly);}}
复制代码

首先说该方法的主要逻辑:

  1. 首先执行 firstTask 的 run 方法。
  2. 然后循环获取阻塞队列中的任务,并调用他们的 run 方法。
  3. 如果线程池中的任务异常,就抛出异常并停止运行线程池。

这个方法可以说就是线程池的核心,在最开始的设定的核心任务数都是直接调用 start 方法启动线程的,启动之后,这个线程并不关闭,而是一直在阻塞队列上等待,如果有任务就执行任务的run 方法,而不是 start 方法,这点很重要。

而该方法中有几个注意的地方就是线程池留给我们扩展的,在执行任务之前,会执行 beforeExecute 方法,该方法默认为空,我们可以实现该方法,在任务执行结束后,在 finally 块中有 afterExecute 方法,同样也是空的,我们可以扩展。

楼主看到这里的代码后,大为赞叹,Doug Lea 可以说神一般的人物。

那么,线程池还有一个方法, submit 是如何实现的呢?其实核心逻辑也是 runWorker 方法,不然楼主也不会说这个方法是线程池的核心。

那我们看看 submit 方法是如何实现的。

4. submit 方法实现原理。

该方法最终也是走 execute 方法的,因此逻辑基本相同,不同的是什么呢?我们看看。我们看到,第二行代码创建了 一个 RunnableFuture 对象,RunnableFuture 是一个接口,具体的实现是什么呢?我们看看:

FutureTask 对象,该对象也是一个线程对象:

那我们就看看该方法的 run 方法。

该方法核心逻辑楼主已经框起来了,其中调用了 call 方法,返回一个返回值,并在set 方法中,将返回值设置在一个变量中,如果是异常,则将异常设置在变量中。我们看看set方法:

该方法通过CAS将任务状态状态从new变成 COMPLETING,然后,设置 outcome 变量,也就是返回值。最后,调用 finishCompletion 方法,完成一些变量的清理工作。

那么,如果从submit 中获得返回值呢?这要看get方法:

该方法会判断状态,如果状态还没有完成,那么就调用 awaitDone 方法等待,如果完成了,调用 report 返回值结果。

看见了刚刚设置的 outcome 变量,如果状态正常,则直接返回,如果状态为取消,则抛出异常,其余情况也抛出异常。

我们回到 awaitDone 方法,看看该方法如何等待的。

该方法有一个死循环,直到有一个确定的状态返回,如果状态大于 COMPLETING ,也就是 成功了,就返回该状态,如果正在进行中,则让出CPU时间片进行等待。如果都不是,则让该线程阻塞等待。在哪里唤醒呢?在 finishCompletion 方法中会唤醒该线程。

该方法循环了等待线程链表的链表,并唤醒链表中的每个线程。

还有一个需要的注意的地方就是,在任务执行完毕会执行 done 方法,JDK 默认是空的,我们可以扩展该方法。比如 Spring 的并发包 org.springframework.util.concurrent 就有2个类重写了该方法。

5. 总结

好了,到这里,线程池的基本实现原理我们知道了,也解开了楼主一直以来的疑惑,可以说,线程池的核心方法就是 runWorker 方法 配合 阻塞队列,当线程启动后,就从队列中取出队列中的任务,执行任务的 run 方法。可以说设计的非常巧妙。而回调线程 callback 也是通过该方法,JDK 封装了 FutureTask 类来执行他们的 call 方法。

good luck!!!!

并发编程之 源码剖析 线程池 实现原理相关推荐

  1. 线程池工作原理流程图 源码概览线程池工作原理流程图 源码概览

    故事讲完啦,再复习下线程池工作流程图吧~ 有兴趣的朋友,源码也看下吧~ if (command == null)throw new NullPointerException();int c = ctl ...

  2. Elasticsearch源码分析—线程池(十一) ——就是从队列里处理请求

    Elasticsearch源码分析-线程池(十一) 转自:https://www.felayman.com/articles/2017/11/10/1510291570687.html 线程池 每个节 ...

  3. JUC源码分析-线程池篇(五):ForkJoinPool - 2

    通过上一篇(JUC源码分析-线程池篇(四):ForkJoinPool - 1)的讲解,相信同学们对 ForkJoinPool 已经有了一个大概的认识,本篇我们将通过分析源码的方式来深入了解 ForkJ ...

  4. 老李推荐:第14章4节《MonkeyRunner源码剖析》 HierarchyViewer实现原理-装备ViewServer-端口转发 1...

    老李推荐:第14章4节<MonkeyRunner源码剖析> HierarchyViewer实现原理-装备ViewServer-端口转发 在初始化HierarchyViewer的实例过程中, ...

  5. 转 Spring源码剖析——核心IOC容器原理

    Spring源码剖析--核心IOC容器原理 2016年08月05日 15:06:16 阅读数:8312 标签: spring 源码 ioc 编程 bean 更多 个人分类: Java https:// ...

  6. 老李推荐:第14章8节《MonkeyRunner源码剖析》 HierarchyViewer实现原理-获取控件列表并建立控件树 1...

    老李推荐:第14章8节<MonkeyRunner源码剖析> HierarchyViewer实现原理-获取控件列表并建立控件树 poptest是国内唯一一家培养测试开发工程师的培训机构,以学 ...

  7. java executor 源码_Java线程池ThreadPoolExecutor深度探索及源码解析

    我们的程序里,时常要使用多线程.因此多线程的管理变的尤为重要.ThreadPoolExecutor很好的解决了这一点.本篇文章主要从源码入手,分析ThreadPoolExecutor的原理. 1.标记 ...

  8. java 线程池 源码_java线程池源码分析

    我们在关闭线程池的时候会使用shutdown()和shutdownNow(),那么问题来了: 这两个方法又什么区别呢? 他们背后的原理是什么呢? 线程池中线程超过了coresize后会怎么操作呢? 为 ...

  9. Java并发(二十一):线程池实现原理

    一.总览 线程池类ThreadPoolExecutor的相关类需要先了解: (图片来自:https://javadoop.com/post/java-thread-pool#%E6%80%BB%E8% ...

最新文章

  1. 系统运维包括哪些内容_智能养老系统包括哪些?养老管理系统内容详解
  2. 交叉熵代价函数——当我们用sigmoid函数作为神经元的激活函数时,最好使用交叉熵代价函数来替代方差代价函数,以避免训练过程太慢...
  3. go语言csv包_玩转数据处理120题R语言版本
  4. 程序员法律考试(3)-依法治国的基本原则和法制体系具体任务
  5. python数字字符是什么_大佬们,想问一下Python中特殊字符的代码是什么啊,只知道数字是digits...
  6. 计算机系统的优化具体操作,win7系统优化提升低配置电脑运行速度的详细技巧...
  7. 时隔二十年,《程序员修炼之道》出第二版了!
  8. 爱客影院源码V3.5.3完整版 无需授权 源码全开源
  9. 怎样用计算机粉碎文件夹,怎么使用电脑文件管家粉碎功能
  10. lora 网关 linux,什么是LoRa网关 如何选择一个好的LoRaWAN网关
  11. 检查Oracle数据库和PDB数据库的大小
  12. 视频必备资源:免费音效素材下载
  13. 【数据治理】数据安全-数据脱敏方案
  14. 计算机网络微课堂笔记
  15. 服务器响应到客户端中文乱码的解决方式
  16. alooa是华为什么型号_pot alooa是华为什么型号 pot alooa是华为麦芒8(图文)
  17. JS中onpropertychange事件和onchange事件区别
  18. file-uploader-cli 关于上传至京东云中文件夹问题的源码修改
  19. Contiki内核原理
  20. Linux文件及日志内容

热门文章

  1. SAP PM 初级系列5 - 工作中心相关的配置
  2. 我身边那些逃离深圳的朋友们
  3. NRFI:网络结点不会指数增加的「神经随机深林模拟」
  4. 强化学习在携程酒店推荐排序中的应用探索
  5. ML.NET 1.1 发布,模型构建器升级和新的异常检测算法
  6. DARPA 计划向下一代人工智能技术投入 20 亿美元
  7. 深度学习中的激活函数总结
  8. 从芯片到系统:FPGA加速卡的发展历程与展望
  9. 圆桌讨论:人工智能的未来
  10. 《数学之美》第17章 由电视剧《暗算》所想到的—谈谈密码学的数学原理