Java是一门多线程的语言,基本上生产环境的Java项目都离不开多线程。而线程则是其中最重要的系统资源之一,如果这个资源利用得不好,很容易导致程序低效率,甚至是出问题。

有以下场景,有个电话拨打系统,有一堆需要拨打的任务要执行,首先肯定是考虑多线程异步去执行。假如我每执行一个拨打任务都new一个Thread去执行,当同时有1万个任务需要执行的时候,那么就会新建1万个线程,加上线程各种初始销毁等操作,这个消耗是巨大的。而其实往往实现这些功能的时候,并不是完全需要实时马上完成,只是希望在可控范围内尽量提高执行的并发性能。

因此线程池技术应用而生,Java中最常用的线程池技术就是ThreadPoolExecutor。接下来就整体看看ThreadPoolExecutor的实现。

这个类的注解非常多,很多也是重点,所以就不从注解开始看起。先从使用说起,有个概念先。

基本使用

        // 核心线程
int corePoolSize = 5;
// 最大线程
int maximumPoolSize = 10;
// 线程空闲回收时间
int keepAliveTime = 30;
// 线程空闲回调时间单位
TimeUnit unit = TimeUnit.SECONDS;
// 队列大小
int queueSize = 20;
// 队列
BlockingQueue workQueue = new ArrayBlockingQueue<Runnable>(queueSize);
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
executor.execute(() -> {
// do something 1
});
executor.execute(() -> {
// do something 2
});

定义好一些必要的参数,构建一个ThreadPoolExecutor对象。然后调用对象的execute()方法即可。

参数说明:

  • corePoolSize,线程池保留的最小线程数。如果线程池中的线程少于此数目,则在执行execut()时创建。
  • maximumPoolSize,线程池中允许拥有的最大线程数。
  • keepAliveTime、unit,当线程闲置时,保持线程存活的时间。
  • workQueue,工作队列,存放提交的等待任务,其中有队列大小的限制。

线程管理机制

非常多人误解了corePoolSize、maximumPoolSize、workQueue的相互关系。不少人认为无论队列选择什么,corePoolSize和maximumPoolSize一定是有用,定义一定是生效的,其实并不然啊!

看下线程基本规则注解说明

  1. 默认情况下,线程池在初始的时候,线程数为0。当接收到一个任务时,如果线程池中存活的线程数小于corePoolSize核心线程,则新建一个线程。
  2. 如果所有运行的核心线程都都在忙,超出核心线程处理的任务,执行器更多地选择把任务放进队列,而不是新建一个线程。
  3. 如果一个任务提交不了到队列,在不超出最大线程数量情况下,会新建线程。超出了就会报错。

另外,如果想在线程初始化时候就有核心线程,可以调用prestartCoreThread()或prestartAllCoreThread(),前者是初始一个,后者是初始全部。

再看看排队策略

  • 直接提交,用SynchronousQueue。特点是不保存,直接提交给线程,如果没没线程,则新建一个。
  • 无限提交,用类似LinkedBlockingQueue无界队列。特点是保存所以核心线程处理不了的任务,队列无上限,最大线程也没用。
  • 有限提交,用类似ArrayBlockingQueue有界队列。特点是可以保存超过核心线程的任务,并且队列也是有上限的。超过上限,新建线程(满了抛错)。更好地保护资源,防止崩溃,也是最常用的排队策略。

从以上规则可以看出来,核心线程数和最大线程数,还有队列结构是相互影响的,如何排队,队列多大,最大线程是多少都是不一定的。

再看看保持存活机制



当超过核心线程数的线程,线程池会让该线程保持存活keepAliveTime时间,超过该时间则会销毁该线程。

另外默认对非核心线程有效,若想核心线程也适用于这个机制,可以调用allowCoreThreadTimeOut()方法。这样的话就没有核心线程这一说了。

综合以上,线程池在多次执行任务后,会一直维持部分线程存活,即使它是闲置的。这样的目的是为了减少线程销毁创建的开销,下次有个任务需要执行,直接从池子里拿线程就能用了。但核心线程不能维护太多,因为也需要一定开销。最大的线程数保护了整个系统的稳定性,避免并发量大的时候,把线程挤满。工作队列则是保证了任务顺序和暂存,系统的可靠性。线程存活规则的目的和维护核心线程的目的类似,但降低了它的存活的时间。

另外还有拒绝机制,它提供了一些异常情况下的解决方案。

ctl线程状态控制

这个ctl变量是整个线程池的核心控制状态。



这个ctl代表了两个变量

  • workerCount,生效的线程数。基本可以理解为存活的线程,但某个时候有暂时性的差异。
  • runState,线程池的运行状态。

    其中,ctl(int32位)的低29位代表workerCount,所以最大线程数为(2^29)-1。另外3位表示runState。

runState有以下几种状态:

  • RUNNING:接收新任务,处理队列任务。
  • SHUTDOWN:不接收新任务,但处理队列任务。
  • STOP:不接收新任务,也不处理队列任务,并且中断所有处理中的任务。
  • TIDYING:所有任务都被终结,有效线程为0。会触发terminated()方法。
  • TERMINATED:当terminated()方法执行结束。

当调用了shutdown(),状态会从RUNNING变成SHUTDOWN,不再接收新任务,此时会处理完队列里面的任务。

如果调用的是shutdownNow(),状态会直接变成STOP。

当线程或者队列都是空的时候,状态就会变成TIDYING。

当terminated()执行完的时候,就会变成TERMINATED。

execute()

带着对上面的规则与机制的认识,现在从就这这个入口开始看看源码,到底整个流程是怎么实现的。

  1. 如果少于核心线程在跑,用这个任务尝试创建一个新线程。
  2. 如果一个任务成功入队,再次检查下线程池状态看是否需要入队,因为可能在入队过程中,状态发送了变化。如果确认入队且没有存活线程,则新建一个空线程。
  3. 如果进不了队,则尝试新建一个线程,如果都失败了。拒绝这个task

    对于第二点最后为什么新建一个线程?很容易猜想到,会有一个轮询的机制让下个task出队,直接利用这个空闲线程。

注释基本解释了所有代码,代码也没什么特别的。其中最主要的还是addWoker()这个方法,下面来看看。

addWoker()

先了解下这个方法的整体思路



从描述可知,addwoker失败,会在线程池状态不对、线程满了或者线程工厂创建线程池失败时候发生。

这个方法比较长,分两段看。先看第一段。



retry:这种写法,如果比较少看源码的,应该是前所未见的了。这是个循环的位置标记,是java的语法之一。看回代码,这里面for循环还嵌套里一个for循环,而retry:是标记第一个for循环的,后面breakcontinue语句都指向到了retry。说明breakcontinue是都是操作外层的for循环。retry可以是任何变量命名合法的字符。

然后看看外出for循环的if语句



这个if判断想要执行到return false;,队列为空是一个必要条件。因为addWork()不单只接收新任务会调用到,处理队列中的任务也会调用到。而前面提到SHUTDOWN状态下还会处理队列中的任务的,所以队列不为空是会让它继续执行下去的。

对于内层的for循环



会先判断worker的数据是否符合corePoolSize和maximumPoolSize的定义,不满足则返回失败。

然后尝试CAS让workerCount自增,如果CAS失败还是继续自旋去自增,直到成功。除非线程池状态发生了变化,发退回到外层for循环重新执行,判断线程池的状态。

第一段的代码,就是让workerCount在符合条件下自增

第二段代码



这段比较好理解,先创建一个Worker对象,这个Worker里面包含一个由线程工厂创建的线程,和一个需要执行的任务(可以为空)。如果线程创建成功了,那么就加一个重入锁去把这个新建的Worker对象放到workers成员变量中,在加入之前需要重新判断下线程池的状态和新建线程的状态。如果worker添加到workers成员变量中,就启动这个新建的线程。最后如果添加失败,则执行addWorkFailed(w)



如果失败了,加锁操作回滚下wokers、workerCount,然后判断下状态看看是否需要终结线程池。

addWorker()大概的流程就这样。

总结

对于其他方法,没有什么特别的,在此不再过多的叙述,有兴趣的可以翻翻源码阅读下。

回顾总结下上面的核心要点

  1. 当核心线程满且忙碌时,线程池倾向于把提交的任务放进队列,而不是新建线程。
  2. 根据选择队列的不同,maximumPoolSize不一定有用的。具体有三种不同的策略。
  3. ctl是线程池的核心控制状态,包含的runState线程池运行状态和workCount有效线程数。
  4. retry:是一种标记循环的语法,retry可以是任何变量命名合法字符。

更多技术文章、精彩干货,请关注

博客:zackku.com

微信公众号:Zack说码

你真的懂ThreadPoolExecutor线程池技术吗?看了源码你会有全新的认识的更多相关文章

  1. 深度分析:Java并发编程之线程池技术,看完面试这个再也不慌了!

    线程池的好处 Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池.在开发过程中,合理地使用线程池,相对于单线程串行处理(Serial Processing ...

  2. java并发编程(四) 线程池 & 任务执行、终止源码分析

    参考文档 线程池任务执行全过程:https://blog.csdn.net/wojiaolinaaa/article/details/51345789 线程池中断:https://www.cnblog ...

  3. java线程池技术(二): 核心ThreadPoolExecutor介绍

    版权声明:本文出自汪磊的博客,转载请务必注明出处. Java线程池技术属于比较"古老"而又比较基础的技术了,本篇博客主要作用是个人技术梳理,没什么新玩意. 一.Java线程池技术的 ...

  4. 用 ThreadPoolExecutor/ThreadPoolTaskExecutor 线程池技术提高系统吞吐量(附带线程池参数详解和使用注意事项)

    1.概述 在Java中,我们一般通过集成Thread类和实现Runnnable接口,调用线程的start()方法实现线程的启动.但如果并发的数量很多,而且每个线程都是执行很短的时间便结束了,那样频繁的 ...

  5. 手写线程池,对照学习ThreadPoolExecutor线程池实现原理!

    作者:小傅哥 博客:https://bugstack.cn Github:https://github.com/fuzhengwei/CodeGuide/wiki 沉淀.分享.成长,让自己和他人都能有 ...

  6. ThreadPoolExecutor 线程池的源码解析

    1.背景介绍 上一篇从整体上介绍了Executor接口,从上一篇我们知道了Executor框架的最顶层实现是ThreadPoolExecutor类,Executors工厂类中提供的newSchedul ...

  7. j.u.c系列(01) ---初探ThreadPoolExecutor线程池

    写在前面 之前探索tomcat7启动的过程中,使用了线程池(ThreadPoolExecutor)的技术 public void createExecutor() { internalExecutor ...

  8. java线程池技术

    1.线程池的实现原理?简介: 多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力.假设一个服务器完成一项任务所需时间为:T1 创建线程时间, ...

  9. 手写一个线程池,带你学习ThreadPoolExecutor线程池实现原理

    摘要:从手写线程池开始,逐步的分析这些代码在Java的线程池中是如何实现的. 本文分享自华为云社区<手写线程池,对照学习ThreadPoolExecutor线程池实现原理!>,作者:小傅哥 ...

随机推荐

  1. iOS 控制台打印unicode 转中文汉字 UTF8String

    今天查看代码数据结构,就在控台直接打印了,soGa,我看到了什么!!!! 于是乎想到了不对劲,不对呀,之前打印都是 UTF8String的呀,怎么会这样,百思不得其姐,看了一下封装的网络类,SoGa, ...

  2. 代码管理工具 (含git、npm、gulp)

    1 Git 分布式代码管理工具(基于Linux,可在本地进行提交)代码同时储存在本地和服务器中 ① Git基本操作命令 (1)初始化,创建初始化仓库 git init ------- 文件初始化,初始 ...

  3. L2-001. 紧急救援---(Dijkstra,记录路径)

    https://www.patest.cn/contests/gplt/L2-001 L2-001. 紧急救援 时间限制 200 ms 内存限制 65536 kB 代码长度限制 8000 B 判题程序 ...

  4. HDU1016 素数环---(dfs)

    http://acm.hdu.edu.cn/showproblem.php?pid=1016 Sample Input 6 8   Sample Output Case 1: 1 4 3 2 5 6 ...

  5. SCC模板

    vector<int> G[maxn]; int pre[maxn], low[maxn], c[maxn]; int n, m; stack<int> s; int dfst ...

  6. 不谈OI:高二(13)最后一次班班有歌声

    最后一次班班有歌声了…… 自己因为搞oi并没有参加,只是决赛的时候幕后放了放背景视频…… 不管怎么说,我们班唱的很棒啊,全部人都很棒! 说真的我为我们班骄傲 视频: https://v.qq.com/ ...

  7. bzoj 2115 路径xor和最大值

    首先我们可以将这张图转化为一棵树,其中可能有好多环,肯定是从1走到N,其中可能经过好多环, 因为我们不走N在的一支,最后还是会走回来,因为a xor b xor b=a,所以相当于没走,就是走了个环, ...

  8. andriod开发增加一个菜单

      第一步: E:\01.prj\pyscrapy\Cet4\res\menu\main.xml <menu xmlns:android="http://schemas.android ...

  9. mysql六:索引原理与慢查询优化

    一 介绍 为何要有索引? 一般的应用系统,读写比例在10:1左右,而且插入操作和一般的更新操作很少出现性能问题,在生产环境中,我们遇到最多的,也是最容易出问题的,还是一些复杂的查询操作,因此对查询语句 ...

  10. android ARM 汇编学习 —— hello world

    android ARM 汇编学习—— 在 android 设备上编译c/cpp代码并用objdump/readelf等工具分析 adb putty 连上手机,用busybox vi 写一个 hello ...