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

关键词:线程池

前言

线程池是大家开发过程中使用频率比较高的组件之一,但是其内部原理又有多少人真正清楚呢。最近抽时间去了解了一下其内部实现细节,感觉略有收获,遂以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. Java之异常的处理(throws)

    import java.io.File;import java.io.FileInputStream;import java.io.FileNotFoundException;import java. ...

  2. JacksonConfig

    package org.linlinjava.litemall.core.config; import com.fasterxml.jackson.annotation.JsonInclude; im ...

  3. 数学中的距离distance(未完成)

    manhattan distance(曼哈顿距离) euclidean distance(欧几里得距离) cosine distance(cosine距离) 闵式距离 切比雪夫距离

  4. Learn Git Lesson06 - 分离头指针

    ============== 知识点 分离头指针 HEAD 含义 git diff 分离头指针 (Detached HEAD) 有时候想尝试性修改某些内容(实验),也许并不会真的提交到分支,这时候可以 ...

  5. [LC] 96. Unique Binary Search Trees

    Given n, how many structurally unique BST's (binary search trees) that store values 1 ... n? Example ...

  6. 吴裕雄--天生自然python学习笔记:pandas模块导入数据

    有时候,手工生成 Pandas 的 DataFrame 数据是件非常麻烦的事情,所以我们通 常会先把数据保存在 Excel 或数据库中,然后再把数据导入 Pandas . 另 一种情况是抓 取网页中成 ...

  7. continuing|offensive

    the fact of something continuing for a long period of timewithout being changed or stopped 连续性,持续性,连 ...

  8. 利用卷积神经网络实现MNIST手写数据识别

    代码: import torch import torch.nn as nn import torch.utils.data as Data import torchvision # 数据库模块 im ...

  9. MyBatis注解及动态Sql

    一.注解实现MyBatis配置 java注解是在jdk1.5版本之后开始加入的,不得不说注解对于我们开发人员来说是个很方便的东西,实现起来也非常的简单,下边我们说一下在MyBatis中使用注解来替换M ...

  10. NIO详解

    目录 NIO 前言 IO与NIO的区别 Buffer(缓冲区) Channel(通道) Charset(字符集) NIO遍历文件 NIO 前言 NIO即New IO,这个库是在JDK1.4中才引入的. ...