作为一名Java开发工程师,想必性能问题是不可避免的。通常,在遇到性能瓶颈时第一时间肯定会想到利用缓存来解决问题,然而缓存虽好用,但也并非万能,某些场景依然无法覆盖。比如:需要实时、多次调用第三方API时,该场景缓存则无法适用。
然多线程并发的方式则很好的解决了上述问题。
 
但若每次都在任务开始时创建新的线程,在任务结束后销毁线程,这样增加了资源的消耗,也降到了响应速度。针对此,线程池通过维护多个线程的方式来降低创建线程的次数,并且对线程的创建、销毁以及数量加以很好的控制,保证对内核的充分利用。
总结起来:
  • 降低了资源的消耗:通过池化技术,重复利用已创建好的线程。
  • 增加了响应的速度:若有空闲线程时,直接执行任务,无需等待创建新的线程。
  • 增加了系统的稳定性和可管理性:对系统而言,过多的线程会导致资源调度失衡,降低了系统的稳定性。线程池进行统一的管理(调优,监控和分配等),减少碎片化信息。

I. 线程池的类关系图

【JDK1.8】
 
从线程池类图关系可见,Java中线程池核心实现类是ThreadPoolExecutor, ThreadPoolExecutor实现的顶层接口是Executor,从图中可以发现:
  • Executor接口只有一个execute()方法,将任务的提交和运行进行解耦。
  • ExecutorService接口在Executor的基础上,增加了生命周期的控制(线程池状态转换)和submit()方法生成Future结果。
  • AbstractExecutorService是一个抽象类,实现了ExecutorService接口中的submit()方法,并实现了任务的执行流程。
  • ThreadPoolExecutor主要实现两个功能:线程池生命周期管理,任务的执行execute()方法。
作为整个架构中最核心的东西-ThreadPoolExecutor,接下来便从源码的角度来分析一二。

II. 生命周期管理

线程池的信息变量ctl是一个原子整型变量,封装了两个概念行字段:
  • workerCount: 表示线程池中有效的线程数量
  • runState: 表示线程池当前的运行状态

ctl信息

源码分析(JDK1.8)java.util.concurrent.ThreadPoolExecutor
COUNT_BITS=29(十进制)
CAPACITY=0000 1111 1111 1111 1111 1111 1111 1111(二进制)

信息获取:

int c = ctl.get();
workerCount = workerCountOf(c); //ctl的低28位表示线程数量
runState = runStateOf(c); //ctl的高四位表示状态

运行状态

源码分析(JDK1.8)java.util.concurrent.ThreadPoolExecutor
 
从源码中可见,线程池主要有5种状态: RUNNING, SHUTDOWN, STOP, TIDYING, TERMINATED,状态之间的关系如下图:
 
  • RUNNING: 可以接受新的任务请求,也可处理阻塞队列中的任务
  • SHUTDOWN: 不接受新的任务请求,但会处理阻塞队列中的任务
  • STOP: 不接受新的任务请求,阻塞队列也会直接清空,正在处理的任务也会被直接中断
  • TIDYING: 所有的任务都已经终止,线程池中不存在有效线程
  • TERMINATED: 线程池终止
从上述状态转换图,我们发现状态的切换主要由shutdown(), shutdownNow(), tryTerminate()以及terminated()几个方法实现。
源码分析(JDK1.8)java.util.concurrent.ThreadPoolExecutor.shutdown()
 
源码分析(JDK1.8)java.util.concurrent.ThreadPoolExecutor.interruptIdleWorkers(): 终止空闲线程。
 
从代码中可以发现,执行shutdown()函数会将线程池状态置为SHUTDOWN状态,并且会终止所有的空闲线程。这里通过java.util.concurrent.ThreadPoolExecutor.Worker.tryLock()方法尝试获取当前线程的AQS独占模式锁,如果目标线程处于空闲状态,则可成功获取锁;反之,若线程正在执行task,则获取锁失败,以此来判断线程是否处于空闲状态。具体的流程图如下:
 
注意: shutdown()函数仅仅是终止了线程池中空闲的线程,正在执行任务的线程依旧可以正常工作,所以处于SHUTDOWN状态下的线程池依旧可以从阻塞队列中获取任务并执行,但不会接受新的task请求(详情见添加worker部分)。
源码分析(JDK1.8)java.util.concurrent.ThreadPoolExecutor.shutdownNow()
 
源码分析(JDK1.8)java.util.concurrent.ThreadPoolExecutor.interruptWorkers(): 终止线程池中所有的线程。
 
源码分析(JDK1.8)java.util.concurrent.ThreadPoolExecutor.Worker.interruptIfStarted(): 中断当前的线程。
 
从代码中可见,执行shutdownNow()函数会将线程池置为STOP状态,终止线程池中所有的线程,由于线程池无法执行task,所以这里会将阻塞对立中所有的task取出并返回。具体的流程图如下:
 
注意:由于所有的线程都被终止了,所以处于STOP状态下的线程池会中断当前正在执行的任务,无法从阻塞队列中获取任务并执行,也无法接受新任务的请求。
源码分析(JDK1.8)java.util.concurrent.ThreadPoolExecutor.tryTerminate()
 
从代码中可见,线程池仅仅处于STOP状态或者SHUTDOWN状态下阻塞队列为空的场景下,才会进一步执行,否则执行结束。需要注意的是,处于SHUTDOWN状态下,虽然阻塞队列为空,但是依然存在线程正在执行任务的可能,所以需要进行二次检查。在将线程池状态置为TIDYING状态前,如果线程池信息发生任何变化,都需要重新检查,避免一些突发异常的发生。具体流程图如下:
 
注意:线程池处于TIDYING状态下会直接执行terminated()方法,默认该方法为空(当然用户可进行自定义重写),之后将线程池状态置为TERMINATED,理论上处于该状态下的线程池已经结束,所以唤醒所有等待线程池结束的线程,执行其任务。
 

III. 运行机制

从图中可见整个运行机制主要分为以下几个模块:
  • 任务请求
  • 任务分配
  • 添加worker
  • 运行worker
  • 任务拒绝
接下来,我们看看对应的源码是如何处理的。

任务请求

ThreadPoolExecutor实现了任务请求和执行的解耦,用户无需关心是如何执行的任务,只需要提供一个可执行的任务,然后调用execute()方法即可。但在实际编码中,我们常常需要获取任务执行的结果,因此,ExecutorService接口在Executor的基础上增加submit()方法,将任务封装成RunnableFuture对象,将执行结果保存为Future对象。综上所言,任务请求有两种方法,分别如下:
  • void execute(Runnable command):不需要获取任务结果。
  • <T> Future<T> submit(Callable<T> task):需要获取任务结果。
execute(Runnable command)见任务分配模块。
submit(Callalbe<T> task)见获取任务结果模块。

任务分配

源码分析(JDK1.8)java.util.concurrent.ThreadPoolExecutor.execute(Runnable command)
 
从代码中可见,执行execute(Runnable command)函数分配任务时主要有以下四个选择:
  • 线程池线程数<核心线程数:创建一条新的线程,并在该线程上直接执行任务;
  • 线程池线程数>=核心线程数 && 阻塞队列未满: 将任务推入阻塞队列中,并创建一条新的线程(若此时线程池存在有效线程则不创建),该线程获取阻塞队列头部的任务并执行;
  • 线程池线程数>=核心线程数 && 阻塞队列已满 && 线程池线程数<最大线程数:创建一条新的线程,并在该线程直接执行任务;
  • 线程池线程数>最大线程数 && 阻塞队列已满:任务拒绝。
详细流程图如下:
 
注意:线程池线程数>=核心线程数 && 阻塞队列未满场景下仅当线程池无有效线程时才会创建新的线程,因为当线程池中线程执行完任务后会再次尝试去阻塞队列获取任务并执行,直到阻塞队列为空才会处于空闲状态。所以,多数场景下此时线程池的线程数=核心线程数,无需创建新的线程。当然,也存在临界场景,比如:此时正好所有的线程都恰好执行完任务并被销毁,为了避免运行异常,则需创建一个新的线程,从阻塞队列中获取任务并执行。

添加worker

源码分析(JDK1.8)java.util.concurrent.ThreadPoolExecutor.addWorker(Runnable firstTask, boolean core)
 
从代码可见,addWorker(Runnable firstTask, boolean core)函数拥有两个入参:
  • firstTask:新创建线程执行的第一个任务,这里特指新提交的任务
  • core:ture表示核心线程数,false表示最大线程数
此外,只有线程池处于(RUNNING状态)或者处于(SHUTDOWN状态+阻塞队列不为空)时,才可以创建worker对象,并且在SHUTDOWN状态下firstTask(新提交的任务)必须为空,才会将新创建的worker对象加入到workers列表中,否则添加worker任务失败。
 
注意:当使用java.util.concurrent.ThreadPoolExecutor.compareAndIncrementWorkerCount()方法尝试登记线程失败时,需要判断下是否已经影响了线程池的状态,如果有,则重新获取线程池状态进行相关校验,若没有,则重新获取线程池线程数量,并进行线程池线程数量的检测。
运行线程任务通过thread.start()方法,即调用了worker对象的run()方法,所以执行worker线程任务会调用worker.run()方法。

运行worker

源码分析(JDK1.8)java.util.concurrent.ThreadPoolExecutor.Worker.run()
 
源码分析(JDK1.8)java.util.concurrent.ThreadPoolExecutor.runWorker(Worker w)
 
从代码可见,
1)runWorker(Worker w)函数会先执行firstTask(如果不为空),等第一个任务执行完会继续尝试从阻塞队列中获取任务,然后继续执行;直到阻塞队列为空。
2)这里并没有使用前面常见的可重入锁ReentranLock,而是使用了自身继承的AQS(AbstractQueuedSynchronizer)锁。当获取任务成功后,目标线程获取AQS独占模式锁,表示该线程处于忙碌状态,直到任务执行完毕才会释放。如果上个任务执行完毕后,目标线程一直无法获取新的任务(阻塞队列为空),则不会获取AQS独占模式锁,表示目标线程处于空闲状态。
3)任务的最终执行还是调用任务的run()方法,所以请求的任务是可执行命令Runnable task.
具体的流程图如下:
 
注意:在实际执行任务前,需要获取线程池的状态,确保此时调用shutdownNow()方法可以及时中断任务。此外,前置任务和后置任务默认为空,用户可自定义重写。

任务拒绝

源码分析(JDK1.8)java.util.concurrent.ThreadPoolExecutor.reject(Runnable command)
 
从代码可见,拒绝任务时调用了handler对象的rejectedExecution()方法。
ThreadPoolExecutor实现了四种不同的策略类,分别为:
源码分析(JDK1.8)java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy:调用主线程(调用ThreadPoolExecuotr的线程)直接执行任务。
 
源码分析(JDK1.8)java.util.concurrent.ThreadPoolExecutor.AbortPolicy: 直接放弃任务,抛出java.util.concurrent.RejectedExecutionException异常。
 
源码分析(JDK1.8)java.util.concurrent.ThreadPoolExecutor.DiscardPolicy: 直接放弃任务,不做任何处理。
 
源码分析(JDK1.8)java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy: 获取阻塞队列头部的任务,并直接将其抛弃;然后,重新请求任务。
 

获取任务结果

在实际编码中,我们常常需要获取任务执行的结果,因此,ExecutorService接口在Executor的基础上增加submit()方法,将任务封装成RunnableFuture对象,将执行结果保存为Future对象。
源码分析(JDK1.8)java.util.concurrent.AbstractExecutorService.submit(Callalbe<T> task)
 
注意:AbstractExecutorrService同样实现submit(Runnable<T> task)方法,这里仅以submit(Callalbe<T> task)方法进行分析。
源码分析(JDK1.8)java.util.concurrent.AbstractExecutorService.newTaskFor(Callable<T> callable): 将任务封装成RunableFuture对象。
 
从上述运行worker模块可知,线程上执行任务时,会调用任务的run()方法,这里封装成java.util.concurrent.FutureTask对象,在线程执行任务时,会调用FutureTask.run()方法
源码分析(JDK1.8)java.util.concurrent.FutureTask.run()
 
源码分析(JDK1.8)java.util.concurrent.FutureTask.set():将结果保存到outcome属性中,以便后面获取结果。
 
源码分析(JDK1.8)java.util.concurrent.FutureTask.get():取出运行结果。
 
注意:这里调用get()方法会一直等待任务执行结束或抛出异常,然后返回结果。
源码分析(JDK1.8)java.util.concurrent.FutureTask.get(long timeout, TimeUnit unit):取出运行结果,超时抛出java.util.concurrent.TimeoutException异常。
 
注意:get(long timeout, TimeUnit unit)中unit表示时间单位(例如:SECONDS),timeout表示时间值。调用该函数获取运行结果时,如果等待时间超时,会直接抛出java.util.concurrent.TimeoutException异常。
具体流程图如下:
 

IV.实际案例

submit(Callable task)

public class MultiThreadPool {
private static List<String> hello = Arrays.asList("h", "e", "l", "l", "o"); private static String task(String args) {
System.out.println(String.format("submit - thread name: %s, args: %s", Thread.currentThread().getName(), args));
return args;
} private static void submitTask() throws InterruptedException, ExecutionException, TimeoutException { List<Future> futures = new ArrayList<>(); final ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 6, 10L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(), new ThreadPoolExecutor.CallerRunsPolicy());
try {
for (String str : hello) {
Thread.sleep(1);
Future f = executor.submit(() -> task(str));
futures.add(f);
}
for (Future f: futures) {
String result = (String) f.get(60, TimeUnit.SECONDS);
System.out.println(String.format("submit - thread name: %s, result: %s", Thread.currentThread().getName(), result));
}
} finally {
executor.shutdown();
}
} public static void main(String[] args) throws Exception {
submitTask();
}
}

输出结果:

submit - thread name: pool-1-thread-3, args: l
submit - thread name: pool-1-thread-1, args: h
submit - thread name: pool-1-thread-4, args: l
submit - thread name: pool-1-thread-2, args: e
submit - thread name: pool-1-thread-3, args: o
submit - thread name: main, result: h
submit - thread name: main, result: e
submit - thread name: main, result: l
submit - thread name: main, result: l
submit - thread name: main, result: o

execute(Runnable task)

public class MultiThreadPool {
private static List<String> hello = Arrays.asList("h", "e", "l", "l", "o"); private static class Task implements Runnable {
private String arg; Task(String arg) {
this.arg = arg;
} public void run() {
System.out.println(String.format("execute - thread name: %s, args: %s", Thread.currentThread().getName(), arg));
}
} private static void executeTask() throws InterruptedException, ExecutionException, TimeoutException {
Map<String, Future> futureMap = new HashMap<>(); final ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 6, 10L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(), new ThreadPoolExecutor.CallerRunsPolicy());
try {
for (String str : hello) {
Thread.sleep(1);
executor.execute(new Task(str));
}
} finally {
executor.shutdown();
}
} public static void main(String[] args) throws Exception {
executeTask();
}
}

输出结果:

execute - thread name: pool-1-thread-3, args: l
execute - thread name: pool-1-thread-1, args: h
execute - thread name: pool-1-thread-4, args: l
execute - thread name: pool-1-thread-2, args: e
execute - thread name: pool-1-thread-3, args: o

作者:吴家二少
本文欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接 

从源码角度来分析线程池-ThreadPoolExecutor实现原理的更多相关文章

  1. Java并发包源码学习系列:线程池ThreadPoolExecutor源码解析

    目录 ThreadPoolExecutor概述 线程池解决的优点 线程池处理流程 创建线程池 重要常量及字段 线程池的五种状态及转换 ThreadPoolExecutor构造参数及参数意义 Work类 ...

  2. Java并发包源码学习系列:线程池ScheduledThreadPoolExecutor源码解析

    目录 ScheduledThreadPoolExecutor概述 类图结构 ScheduledExecutorService ScheduledFutureTask FutureTask schedu ...

  3. JUC源码学习笔记5——线程池,FutureTask,Executor框架源码解析

    JUC源码学习笔记5--线程池,FutureTask,Executor框架源码解析 源码基于JDK8 参考了美团技术博客 https://tech.meituan.com/2020/04/02/jav ...

  4. 《java.util.concurrent 包源码阅读》13 线程池系列之ThreadPoolExecutor 第三部分

    这一部分来说说线程池如何进行状态控制,即线程池的开启和关闭. 先来说说线程池的开启,这部分来看ThreadPoolExecutor构造方法: public ThreadPoolExecutor(int ...

  5. Java8线程池ThreadPoolExecutor底层原理及其源码解析

    小侃一下 日常开发中, 或许不会直接new线程或线程池, 但这些线程相关的基础或思想是非常重要的, 参考林迪效应; 就算没有直接用到, 可能间接也用到了类似的思想或原理, 例如tomcat, jett ...

  6. jdk线程池ThreadPoolExecutor工作原理解析(自己动手实现线程池)(一)

    jdk线程池ThreadPoolExecutor工作原理解析(自己动手实现线程池)(一) 线程池介绍 在日常开发中经常会遇到需要使用其它线程将大量任务异步处理的场景(异步化以及提升系统的吞吐量),而在 ...

  7. 线程池ThreadPoolExecutor使用原理

    本文来源于翁舒航的博客,点击即可跳转原文观看!!!(被转载或者拷贝走的内容可能缺失图片.视频等原文的内容) 若网站将链接屏蔽,可直接拷贝原文链接到地址栏跳转观看,原文链接:https://www.cn ...

  8. 21.线程池ThreadPoolExecutor实现原理

    1. 为什么要使用线程池 在实际使用中,线程是很占用系统资源的,如果对线程管理不善很容易导致系统问题.因此,在大多数并发框架中都会使用线程池来管理线程,使用线程池管理线程主要有如下好处: 降低资源消耗 ...

  9. 源码解读 TDengine 中线程池的实现

    这篇文章中提到了 tsched 的源码可以一读,所以去阅读了一下,总共220来行. 1. 阅读前工作 通过上文了解到这段程序实现的是一个任务队列,同时带有线程池.这段程序是计算机操作系统里经典的con ...

随机推荐

  1. Thymeleaf中model设一个值 页面显示此值 JS取此值

    model设值: m.addAttribute("pageNo", pageNo); 页面显示值: 当前为第:<span th:text="${pageNo}&qu ...

  2. 转载:[Oracle]杀死正在执行的sql语句

    地址:https://www.jianshu.com/p/7ad9ce2db55c 还未尝试,试后再来总结. 2020年2月12日 20点19分 试了,不好用.

  3. JVM 中的对象及引用

    JVM中对象的创建过程 对象的内存分配 虚拟机遇到一条 new 指令时,首先检查是否被类加载器加载,如果没有,那必须先执行相应的类加载过程. 类加载就是把 class 加载到 JVM 的运行时数据区的 ...

  4. 转载:Linux: What’s the difference between a soft link and a hard link?

    Link:https://www.moreofless.co.uk/linux-difference-soft-symbolic-link-and-hard-link/ This example sh ...

  5. ICARUS主题美化

    Icarus用户指南 - 主题美化 Icarus的主题样式编码文件为themes/icarus/layout/layout.jsx. 此文件定义了站点全局的样式设置.本文详细介绍了本主题针对文章分类的 ...

  6. CTF-WeChall-第二天

    2020.09.10 奥力给,举步维艰的时候就是要一边做一遍记,虽然慢但是不要嫌弃,要不然就是举步不前

  7. outh2

    之前做天猫精灵对接,就碰到了outh鉴权,当时实现好之后没有细细缕,今天看了一个博主的介绍,贴一下 转载自http://www.ruanyifeng.com/blog/2014/05/oauth_2_ ...

  8. 【转】Locust性能-零基础入门系列(3)-压力权重

    本文将继续对Locust性能测试进行持续讲解,主要是讲解虚拟用户数分配和权重的关系.在locust file中进行多用户类的实现和操作.我们这次先上完整的代码: from locust import ...

  9. linux学习(十一)linux安装nginx

    一.前言 由于本地练手的小demo用的是vue+spring boot来玩的,所以部署的时候想着用Nginx来实现反向代理[即请求转发,解决前后端分离的跨域请求问题],既然要用,那么首先得在服务器上面 ...

  10. MySQL中的临时表到底什么是?

    Author:极客小俊 一个专注于web技术的80后 我不用拼过聪明人,我只需要拼过那些懒人 我就一定会超越大部分人! CSDN@极客小俊,原创文章, B站技术分享 B站视频 : Bilibili.c ...