导读:线程池是开发中使用频率比较高的组件之一,但是又有多少人真正了解其内部机制呢。

关键词:线程池

前言

线程池是大家开发过程中使用频率比较高的组件之一,但是其内部原理又有多少人真正清楚呢。最近抽时间去了解了一下其内部实现细节,感觉略有收获,遂以ThreadPoolExecuter为例将自己的心得体会分享出来和大家一起交流,如有不妥之处,烦请大家积极指正。

我的疑问

  1. 执行任务的工作机制是怎样的?
  2. 是什么时机清理空闲线程的?
  3. ThreadPoolExecutor.CallerRunsPolicy 什么场景下会用到?
  4. 线程池终止的时候都做了什么?(过两天补上)

问题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初探的更多相关文章

  1. java并发初探ThreadPoolExecutor拒绝策略

    java并发初探ThreadPoolExecutor拒绝策略 ThreadPoolExecuter构造器 corePoolSize是核心线程池,就是常驻线程池数量: maximumPoolSize是最 ...

  2. java并发初探ConcurrentSkipListMap

    java并发初探ConcurrentSkipListMap ConcurrentSkipListMap以调表这种数据结构以空间换时间获得效率,通过volatile和CAS操作保证线程安全,而且它保证了 ...

  3. java并发初探ConcurrentHashMap

    java并发初探ConcurrentHashMap Doug Lea在java并发上创造了不可磨灭的功劳,ConcurrentHashMap体现这位大师的非凡能力. 1.8中ConcurrentHas ...

  4. java并发初探CyclicBarrier

    java并发初探CyclicBarrier CyclicBarrier的作用 CyclicBarrier,"循环屏障"的作用就是一系列的线程等待直至达到屏障的"瓶颈点&q ...

  5. java并发初探CountDownLatch

    java并发初探CountDownLatch CountDownLatch是同步工具类能够允许一个或者多个线程等待直到其他线程完成操作. 当前前程A调用CountDownLatch的await方法进入 ...

  6. java并发初探ReentrantWriteReadLock

    java并发初探ReentrantWriteReadLock ReenWriteReadLock类的优秀博客 ReentrantReadWriteLock读写锁详解 Java多线程系列--" ...

  7. Java内部类初探

    Java内部类初探 之前对内部类的概念不太清晰,在此对内部类与外部类之间的关系以及它们之间的调用方式进行一个总结. Java内部类一般可以分为以下三种: 成员内部类 静态内部类 匿名内部类 一.成员内 ...

  8. JAVA ThreadPoolExecutor(转)

    原文链接:http://blog.csdn.net/historyasamirror/article/details/5961368 基础 在我看来,java比C++的一个大好处就是提供了对多线程的支 ...

  9. 浅谈JAVA ThreadPoolExecutor(转)

    这篇文章分为两部分,前面是ThreadPoolExecutor的一些基本知识,后一部分则是Mina中一个特殊的ThreadPoolExecutor代码解析.算是我的Java学习笔记吧. 基础 在我看来 ...

随机推荐

  1. k-means|k-mode|k-prototype|PAM|AGNES|DIANA|Hierarchical cluster|DA|VIF|

    聚类算法: 对于数值变量,k-means eg:k=4,则选出不在原数据中的4个点,计算图形中每个点到这四个点之间的距离,距离最近的便是属于那一类.标准化之后便没有单位差异了,就可以相互比较. 对于分 ...

  2. windows系统安装msi文件总提示2502、2503的错误

    首先: 1.按WIN+R,在运行框中输入“gpedit.msc” 确认:2.打开本地策略组编辑器后依次展开 :“计算机配置”->“管理模板”->“windows组件”->“windo ...

  3. pom配置项目build jar 包含源码的配置

    <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> ...

  4. Java复习(一)——Java语言概述、开发环境、基础知识

    设计模式:在各种应用系统中被大量应用,是设计的“常用套路” 作为Java WEB开发人员,需要掌握HTML/CSS/JS和数据库相关知识 常用的应用程序框架:Spring MVC,Spring Boo ...

  5. Perl: 单引号里面的直接给当做标量了,而直接输出($`)的话就是变量值,即相符段落的前置字符会存到这里。输出‘$`’ 就变成标量值了

    print '$`'."\n";print '$&'."\n";print $'."\n"; 输出: $`$& 而直接输出( ...

  6. bind() 方法

    一. 定义和用法 bind() 方法为被选元素添加一个或多个事件处理程序,并规定事件发生时运行的函数. 语法: $(selector).bind(event,data,function) 举例:  

  7. python学习笔记(24)-类与对象

    #类与对象 #python类的语法 关键字 class #class 类名 类名的规范是:数字字母下划线组成,不能以数字开头 首字母大写 驼峰命名 #类属性 放在类里面的变量值 #类方法 放在类里面的 ...

  8. 复杂的Polygon

  9. [LC] 222. Count Complete Tree Nodes

    Given a complete binary tree, count the number of nodes. Note: Definition of a complete binary tree ...

  10. docker 不同引擎导致历史垃圾镜像无法自动清除,致硬盘空间报警

    查看硬盘占用大户是/var/lib/docker/vfs/dir 直觉是images文件,历史原因累积了大量的image docker rmi 清除掉不用的image文件 可用空间有提升但提升不大 / ...