引言

上一篇文章我们有介绍过线程池的一个基本执行流程《【Java并发编程】面试必备之线程池》以及它的7个核心参数,以及每个参数的作用、以及如何去使用线程池 还留了几个小问题。。建议看这篇文章之前可先看下前面那篇文章。这篇文章我们就来分析下上篇文章的几个小问题

  • 线程池是否区分核心线程和非核心线程?

  • 如何保证核心线程不被销毁?

  • 线程池的线程是如何做到复用的?

我们先看最后一个问题一般一个线程执行完任务之后就结束了,Thread.start()只能调用一次,一旦这个调用结束,则该线程就到了stop状态,不能再次调用start。如果你对一个已经启动的线程对象再调用一次start方法的话,会产生:IllegalThreadStateException异常,但是Threadrun方法是可以重复调用的。

所以这里也会有一个面试经常问到的问题:「Thread类中run()和start()方法的有什么区别?」下面我们就从jdk的源码来一起看看如何实现线程复用的:线程池执行任务的ThreadPoolExecutor#execute方法为入口

 public void execute(Runnable command) {if (command == null)throw new NullPointerException();int c = ctl.get();// 线程池当前线程数小于 corePoolSize 时进入if条件调用 addWorker 创建核心线程来执行任务if (workerCountOf(c) < corePoolSize) {if (addWorker(command, true))return;c = ctl.get();}// 线程池当前线程数大于或等于 corePoolSize ,就将任务添加到 workQueue 中if (isRunning(c) && workQueue.offer(command)) {// 获取到当前线程的状态,赋值给 recheck ,是为了重新检查状态int recheck = ctl.get();// 如果 isRunning 返回 false ,那就 remove 掉这个任务,然后执行拒绝策略,也就是回滚重新排队if (! isRunning(recheck) && remove(command))reject(command);// 线程池处于 running 状态,但是没有线程,那就创建线程执行任务else if (workerCountOf(recheck) == 0)addWorker(null, false);}// 如果任务放入 workQueue 失败,则尝试通过创建非核心线程来执行任务// 创建非核心线程失败,则说明线程池已经关闭或者已经饱和,会执行拒绝策略else if (!addWorker(command, false))reject(command);}

「excute」方法主要业务逻辑

  • 如果当前的线程池运行线程小于「coreSize」,则创建新线程来执行任务。

  • 如果当前运行的线程等于「coreSize」或多余「coreSize」(动态修改了coreSize才会出现这种情况),把任务放到阻塞队列中。

  • 如果队列已满无法将新加入的任务放进去的话,则需要创建新的线程来执行任务。

  • 如果新创建线程已经达到了最大线程数,任务将会被拒绝。

addWorker 方法

上述方法的核心主要就是addWorker方法,

private boolean addWorker(Runnable firstTask, boolean core) {// 前面还有一部分就省略了。。。。boolean workerStarted = false;boolean workerAdded = false;Worker w = null;try {w = new Worker(firstTask);final Thread t = w.thread;if (t != null) {final ReentrantLock mainLock = this.mainLock;mainLock.lock();try {// Recheck while holding lock.// Back out on ThreadFactory failure or if// shut down before lock acquired.int rs = runStateOf(ctl.get());if (rs < SHUTDOWN ||(rs == SHUTDOWN && firstTask == null)) {if (t.isAlive()) // precheck that t is startablethrow new IllegalThreadStateException();workers.add(w);int s = workers.size();if (s > largestPoolSize)largestPoolSize = s;workerAdded = true;}} finally {mainLock.unlock();}if (workerAdded) {t.start();workerStarted = true;}}} finally {if (! workerStarted)addWorkerFailed(w);}return workerStarted;}

这个方法我们先看看这个「work」类吧

private final class Workerextends AbstractQueuedSynchronizerimplements Runnable{Worker(Runnable firstTask) {setState(-1); // inhibit interrupts until runWorkerthis.firstTask = firstTask;this.thread = getThreadFactory().newThread(this);}public void run() {runWorker(this);}

「work」类实现了「Runnable」接口,然后run方法里面调用了「runWorker」方法

final void runWorker(Worker w) {Thread wt = Thread.currentThread();// 新增创建Runnable task = w.firstTask;w.firstTask = null;w.unlock(); // allow interruptsboolean completedAbruptly = true;try {// 判断 task 是否为空,如果不为空直接执行// 如果 task 为空,调用 getTask() 方法,从 workQueue 中取出新的 task 执行while (task != null || (task = getTask()) != null) {w.lock();if ((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);}}

这个runwork方法中会优先取worker绑定的任务,如果创建这个worker的时候没有给worker绑定任务,worker就会从队列里面获取任务来执行,执行完之后worker并不会销毁,而是通过while循环不停的执行getTask方法从阻塞队列中获取任务调用task.run()来执行任务,这样的话就达到了线程复用的目的。

while (task != null || (task = getTask()) != null) 这个循环条件只要getTask 返回获取的值不为空这个循环就不会终止, 这样线程也就会一直在运行。「那么任务执行完怎么保证核心线程不销毁?非核心线程销毁?」答案就在这个getTask()方法里面

private Runnable getTask() {// 超时标记,默认为false,如果调用workQueue.poll()方法超时了,会标记为true// 这个标记非常之重要,下面会说到boolean timedOut = false;for (;;) {// 获取ctl变量值int c = ctl.get();int rs = runStateOf(c);// 如果当前状态大于等于SHUTDOWN,并且workQueue中的任务为空或者状态大于等于STOP// 则操作AQS减少工作线程数量,并且返回null,线程被回收// 也说明假设状态为SHUTDOWN的情况下,如果workQueue不为空,那么线程池还是可以继续执行剩下的任务if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {// 操作AQS将线程池中的线程数量减一decrementWorkerCount();return null;}// 获取线程池中的有效线程数量int wc = workerCountOf(c);// 如果主动开启allowCoreThreadTimeOut,或者获取当前工作线程大于corePoolSize,那么该线程是可以被超时回收的// allowCoreThreadTimeOut默认为false,即默认不允许核心线程超时回收// 这里也说明了在核心线程以外的线程都为“临时”线程,随时会被线程池回收boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;// 这里说明了两点销毁线程的条件:// 1.原则上线程池数量不可能大于maximumPoolSize,但可能会出现并发时操作了setMaximumPoolSize方法,如果此时将最大线程数量调少了,很可能会出现当前工作线程大于最大线程的情况,这时就需要线程超时回收,以维持线程池最大线程小于maximumPoolSize,// 2.timed && timedOut 如果为true,表示当前操作需要进行超时控制,这里的timedOut为true,说明该线程已经从workQueue.poll()方法超时了// 以上两点满足其一,都可以触发线程超时回收if ((wc > maximumPoolSize || (timed && timedOut))&& (wc > 1 || workQueue.isEmpty())) {// 尝试用AQS将线程池线程数量减一if (compareAndDecrementWorkerCount(c))// 减一成功后返回null,线程被回收return null;// 否则循环重试continue;}try {// 如果timed为true,阻塞超时获取任务,否则阻塞获取任务Runnable r = timed ?workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :workQueue.take();if (r != null)return r;// 如果poll超时获取任务超时了, 将timeOut设置为true// 继续循环执行,如果碰巧开发者开启了allowCoreThreadTimeOut,那么该线程就满足超时回收了timedOut = true;} catch (InterruptedException retry) {timedOut = false;}}
}

所以保证线程不被销毁的关键代码就是这一句代码

   Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();

只要timedfalse这个workQueue.take()就会一直阻塞,也就保证了线程不会被销毁。timed的值又是通过allowCoreThreadTimeOut和正在运行的线程数量是否大于coreSize控制的。

  • 只要getTask方法返回null 我们的线程就会被回收(runWorker方法会调用processWorkerExit)

  • 这个方法的源码也就解释了为什么我们在创建线程池的时候设置了allowCoreThreadTimeOut =true的话,核心线程也会进行销毁。

  • 通过这个方法我也们可以回答上面那个问题线程池是不区分核心线程和非核心线程的。

END

推荐好文

强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

为什么MySQL不推荐使用uuid或者雪花id作为主键?

为什么建议大家使用 Linux 开发?爽(外加七个感叹号)

IntelliJ IDEA 15款 神级超级牛逼插件推荐(自用,真的超级牛逼)

炫酷,SpringBoot+Echarts实现用户访问地图可视化(附源码)

记一次由Redis分布式锁造成的重大事故,避免以后踩坑!

十分钟学会使用 Elasticsearch 优雅搭建自己的搜索系统(附源码)

Java并发编程:从源码分析几道必问线程池的面试题?相关推荐

  1. Java并发编程-ReentrantLock源码分析

    一.前言 在分析了 AbstractQueuedSynchronier 源码后,接着分析ReentrantLock源码,其实在 AbstractQueuedSynchronizer 的分析中,已经提到 ...

  2. Java并发编程 ReentrantLock 源码分析

    ReentrantLock 一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大. 这个类主要基于AQS(Abst ...

  3. 【java】java 并发编程 Condition 源码分析

    文章目录 1.概述 1.2 案例 3.2 实现方法顺序调用 2.源码解析 2.1 接口方法 2.2 继承 2.3 队列 2.4 await 分析 2.4.1 线程1 await 2.4.2 线程2 a ...

  4. 【java】java 并发编程 CyclicBarrier 源码分析

    文章目录 1.概述 4.源码阅读 4.1 构造方法 4.2 Generation 4.3 属性Condition 4.4 await方法 4.4.1 BrokenBarrierException 4. ...

  5. Java并发编程 LockSupport源码分析

    这个类比较简单,是一个静态类,不需要实例化直接使用,底层是通过java未开源的Unsafe直接调用底层操作系统来完成对线程的阻塞. 1 package java.util.concurrent.loc ...

  6. java futuretask 源码_java并发编程——FutureTask源码分析

    FutureTask的简单示例: FutureTask的应用场景,如果在当前线程中需要执行比较耗时的操作,但又不想阻塞当前线程时,可以把这些作业交给FutureTask,另开一个线程在后台完成,当当前 ...

  7. Java并发编程-AQS源码之条件队列

    System.out.println(name + "==>成功获取到锁" + lock); try { condition.await(); } catch (Interr ...

  8. java function获取参数_「Java容器」ArrayList源码,大厂面试必问

    ArrayList简介 ArrayList核心源码 ArrayList源码分析 System.arraycopy()和Arrays.copyOf()方法 两者联系与区别 ArrayList核心扩容技术 ...

  9. java futuretask 源码解析_Java异步编程——深入源码分析FutureTask

    Java的异步编程是一项非常常用的多线程技术. 之前通过源码详细分析了ThreadPoolExecutor<你真的懂ThreadPoolExecutor线程池技术吗?看了源码你会有全新的认识&g ...

最新文章

  1. 一致 先验分布 后验分布_浅谈Loki分布式架构中的一致性哈希
  2. python列表解析的新方法
  3. jenkins+git+maven搭建项目自动化持续集成
  4. selenium webdriver(python)_selenium、webdriver及浏览器的关系及对应版本安装
  5. 解决网络请求的依赖关系
  6. MSP430F5529 DriverLib 库函数学习笔记(十三)认识低功耗模式
  7. jqgrid columnChooser列的自定义及存储和获取
  8. Rust布道者张汉东倾授,入门Rust初学者都要攻破哪些难点?
  9. .net framework 4.0安装_R4.0的源码安装——以mac为例
  10. 一次数据库上云迁移性能下降的排查
  11. centos下卸载php,centos如何完全卸载php
  12. [C#]System.Timers.Timer(2)
  13. UESTC 574 High-level ancients
  14. Map按照key的ASCII码排序
  15. Kafka原理以及分区分配策略剖析
  16. 30分钟读懂Linux五大模块内核源码,内核整体架构设计
  17. 儿童摄影html代码源,HTML5织梦dede儿童摄影/影楼/写真/摄影工作室网站模板
  18. Qt 之 QCustomPlot(图形库)
  19. print中的逗号“,”打印出来相当于空格
  20. 【工作】论文格式详细要求

热门文章

  1. 支付宝App采用华为方舟编译器几乎秒开?支付宝回应:华为好棒,加油
  2. 放弃耳机孔、放弃按键的手机我们是怎么接受并习惯的?
  3. 中国铁路官宣:高铁Wi-Fi将融合5G技术
  4. fatal: protocol error: bad line length character: No s原因
  5. 离开HK后的第一篇所感--重生
  6. APP后端数据接口注意事项
  7. 从输入 URL 到页面加载完的过程中都发生了什么事情?
  8. android 录音命令,音频延迟  |  Android NDK  |  Android Developers
  9. openssl 基本算法小例
  10. 目前阶段的任务及计划