网上介绍线程池的文章很多,质量好坏不一。能讲的很透彻的,确实不多。

本人能力有限,本文先从原理入手,讲清楚线程池是怎么运行的。

至于源码的分析,将单独写一篇(《线程池源码详解》)。

全文以 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() 方法的四大步

  1. 接到任务,先创建工作线程
  2. 工作线程达到一定数量,把任务往阻塞队列放
  3. 阻塞队列放不下了,创建工作线程(不超过最大线程数)
  4. 如果前三步应付不过来,执行拒绝策略

线程工作,即 addWorker() 方法执行里,调用 Thread 类的 start() 方法,JVM 会调用 run() 方法

重写的 run() 方法执行 runWorker() 方法,此方法会循环取任务,直到没有任务了。

随后会维护线程池里线程的数量。最后是关闭线程池。

看了之后,有没有一种错觉,线程池的代码挺简单的呀,没有什么复杂的逻辑呀!

事实是代码巨复杂,这篇文章是把代码简化了,只说大流程。

有了这篇的基础,下篇解析源码时,会详细说明

  • 线程池状态是怎么控制的
  • 创建线程是怎么防止并发的
  • 核心线程数和最大线程数,具体干啥的
  • 哪些地方用了 AQS 框架
  • 线程最长存活时间,是怎样控制线程生命周期的
  • addWorker 时,传一个空任务,是干嘛的
  • 常用线程池,是怎么选阻塞队列的
  • ……
    所有这些,请看下篇《线程池源码详解》。

图解线程池——清新脱俗的讲原理相关推荐

  1. 深入理解线程池(ThreadPoolExecutor)——细致入微的讲源码。

    在上一篇博文<图解线程池原理>中,大体上介绍了线程池的工作原理. 这一篇从源码层面,细致剖析,文章会很长. 如果上篇文章内容没吸收,先看上篇,先易后难嘛. 本文源码是 java 1.8 版 ...

  2. Java线程池,从使用到原理

    转载自  Java线程池,从使用到原理 线程池的技术背景 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源.在Java中更是如此,虚拟机将试图跟踪每一个对象 ...

  3. java线程池的工作原理_Java 线程池的介绍以及工作原理

    在什么情况下使用线程池? 1.单个任务处理的时间比较短 2.将需处理的任务的数量大 使用线程池的好处: 1. 降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的消耗. 2. 提高响应速度 ...

  4. Java 线程池的介绍以及工作原理

    在什么情况下使用线程池? 1.单个任务处理的时间比较短 2.将需处理的任务的数量大 使用线程池的好处: 1. 降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的消耗. 2. 提高响应速度 ...

  5. 线程池作用、用法以及原理

    线程池 作用 用法 建议设定大小 快捷构造线程池 submit与execute shutdown与shutdownNow Future与FutureTast 代码 状态 底层原理 继承关系 主要参数 ...

  6. JUC多线程:线程池的创建及工作原理 和 Executor 框架

    一.什么是线程池: 线程池主要是为了解决 新任务执行时,应用程序为任务创建一个新线程 以及 任务执行完毕时,销毁线程所带来的开销.通过线程池,可以在项目初始化时就创建一个线程集合,然后在需要执行新任务 ...

  7. java map集合 事务控制_对象回收过程?线程池执行过程? map原理?集合类关系?synchronized 和 volatile ? 同一个类的方法事务传播控制还有作用吗?java 锁...

    1.  对象回收过程? 可达性分析算法: 如果一个对象从 GC Roots 不可达时,则证明此对象不可用. 通过一系列称为GC ROOTS的对象作为起点,从这些起点往下搜索,搜索走过的路径 称为引用链 ...

  8. 给女朋友讲 : Java线程池的内部原理

    文章持续更新,微信搜索「 万猫学社 」第一时间阅读. 关注后回复「 电子书 」,免费获取12本Java必读技术书籍. 餐厅的约会 餐盘在灯光的照耀下格外晶莹洁白,女朋友拿起红酒杯轻轻地抿了一小口,对我 ...

  9. ScheduleThreadPoolExecutor 定时任务线程池原理

    ScheduleThreadPoolExecutor 定时任务线程池 ScheduleThreadPoolExecutor 定时任务线程池 定时任务实现方法 实现原理 ScheduleFutureTa ...

最新文章

  1. 源码资本张宏江:只有算法和技术,那你一定挣不到钱
  2. fedora如何隐藏顶部状态栏_如何使用PDF Arranger来对PDF文件进行排版和修改
  3. Windows10 家庭版没有本地组策略解决方法
  4. flash 遮罩层全解
  5. Android持久化存储(2)SharedPreferences使用介绍
  6. 波特率、信息传输速率与带宽的关系
  7. 解决java报Too many open files错误
  8. DFS 下沙小面的(2)
  9. beetl html模板,Spring Boot 2 中使用 beetl 模板引擎
  10. cs229 机器学习
  11. html动画3d背景图片,jQuery和CSS3全屏3D背景图片视觉差特效
  12. 前端自学学习笔记——JavaScript
  13. jquery1.7版本核心模块测试封装
  14. ABAQUS均匀受力,边界条件对称,但结果云图不对称
  15. android每天定时打卡,钉钉定时打卡脚本下载|叉叉助手钉钉定时打卡插件安卓版下载 v4.3.1 - 跑跑车安卓网...
  16. 用 matplotlib 做交互式的票房分析
  17. html5动效系列二:超级惊艳 10款HTML5动画特效推荐
  18. Rubby在青柠互动的一月份工作总结
  19. 技术:双电脑共享鼠标、键盘解决方案 | USB对拷线、Synergy
  20. [常系数(非)齐次线性递推]

热门文章

  1. 重磅!解密央行数字货币研究所第二任所长穆长春(附完整个人简历)
  2. shell快捷键总结
  3. 2021年电工(中级)新版试题及电工(中级)实操考试视频
  4. 信用机制的发展与区块链的诞生
  5. js replace 全部替换
  6. Bypassing the Monster: A Faster and Simpler Optimal Algorithm for Contextual Bandits under Realizabi
  7. js关于indexOf的使用要注意的问题
  8. 书名: 岛上书店 简评:一个书店将各个人物的生活事业联系起来。
  9. 怎样把flv转换成mp4,4种方法轻松学
  10. 做闲鱼两年了,bug不少