首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164…
Github : 👉 github.com/black-ant
CASE 备份 : 👉 gitee.com/antblack/ca…
线程池这东西 , 用很简单 , 想用好 , 不容易啊~~
一 . 线程池简介
1 线程池的元素
线程池主要由两个概念组成,一个是任务队列,另一个是工作者线程。
- 任务队列是一个阻塞队列,保存待执行的任务。
- 工作者线程主体就是一个循环,循环从队列中接受任务并执行。
2 为什么要用线程池
- 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
3 线程池中的核心概念
- BlockingQueue workQueue : 用于保留任务并移交给工作线程的队列
- HashSet workers : 线程池中所有的工作线程
4 线程池的原理定义 :
线程池通过一个叫 ctl 的 AtomicInteger 决定运行情况 , 通过 ThreadFactory 创建线程 , 并且把等待的线程放入 workQueue , 等待移交给工作线程
二. 常见的线程池
1 | java复制代码// 基本对象 |
三. 线程池的创建
线程池创建可以通过 ThreadPoolExecutor 和 工具类 Executors 实现
3.1 通过构造方法实现(推荐)
通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则
3.2 通过Executor 框架的工具类Executors来实现 (个人demo 可以考虑)
3.2.1 FixedThreadPool
1 | java复制代码return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()); |
- 该方法返回一个固定线程数量的线程池。 (corePoolSize == maximumPoolSize)
- 使用LinkedBlockingQuene作为阻塞队列
- 当线程池没有可执行任务时,也不会释放线程
- 该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
3.2.2 SingleThreadExecutor
1 | java复制代码return new FinalizableDelegatedExecutorService ( |
- 方法返回一个只有一个线程的线程池。
- 若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
3.2.3 CachedThreadPool:
1 | java复制代码return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>()); |
- 该方法返回一个可根据实际情况调整线程数量的线程池。(默认缓存60s , 线程池的线程数可达到Integer.MAX_VALUE,即2147483647)
- 内部使用SynchronousQueue作为阻塞队列
- 线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。
- 所有线程在当前任务执行完毕后,将返回线程池进行复用。
- 在没有任务执行时,当线程的空闲时间超过keepAliveTime,会自动释放线程资源
3.2.4 ScheduledExecutorService :
1 | java复制代码return new DelegatedScheduledExecutorService(new ScheduledThreadPoolExecutor(1)); |
- 初始化的线程池可以在指定的时间内周期性的执行所提交的任务,在实际的业务场景中可以使用该线程池定期的同步数据
四. Fork/Join
1 | java复制代码1 > Fork / Join 的核心是 ForkJoinPool , 用于来管理工作线程 |
五. ThreadPoolExecutor
1 | java复制代码ThreadPoolExecutor实现了生产者/消费者模式, |
六 . 线程池的饱和和动态调整
1 | java复制代码// 线程池的饱和策略 , 当线程池满了. 会通过对应的策略 |
七. 线程池执行任务的过程
刚创建时,里面没有线程调用 execute() 方法,添加任务时:
- 如果正在运行的线程数量小于核心参数 corePoolSize ,继续创建线程运行这个任务
+ 否则,如果正在运行的线程数量大于或等于 corePoolSize ,将任务加入到阻塞队列中。
+ 否则,如果队列已满,同时正在运行的线程数量小于核心参数 maximumPoolSize ,继续创建线程运行这个任务。
+ 否则,如果队列已满,同时正在运行的线程数量大于或等于 maximumPoolSize ,根据设置的拒绝策略处理。
- 完成一个任务,继续取下一个任务处理。
+ 没有任务继续处理,线程被中断或者线程池被关闭时,线程退出执行,如果线程池被关闭,线程结束。
+ 否则,判断线程池正在运行的线程数量是否大于核心线程数,如果是,线程结束,否则线程阻塞。因此线程池任务全部执行完成后,继续留存的线程池大小为 corePoolSize 。
八. 线程池中 submit 和 execute 方法有什么区别
两个方法都可以向线程池提交任务。
- #execute(…) 方法,返回类型是 void ,它定义在 Executor 接口中 , 必须实现Runnable接口 。
- #submit(…) 方法,可以返回持有计算结果的 Future 对象,它定义在 ExecutorService 接口中,它扩展了 Executor 接口,其它线程池类像 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 都有这些方法。
九 . 如果你提交任务时,线程池队列已满,这时会发生什么
重点在于线程池的队列是有界还是无界的。
如果你使用的 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务。
如果你使用的是有界队列比方说 ArrayBlockingQueue 的话,任务首先会被添加到 ArrayBlockingQueue 中,ArrayBlockingQueue满了,则会使用拒绝策略 RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy 。
十 . 线程池的底层逻辑
要想弄清楚这一部分 , 首先得理解 Queue , Worker , Task , Thread 等多个概念
- Queue :
- Worker :
- Task :
- Thread :
10.1 线程池的物理结构
Worker 对象
Worker 对象是 ThreadPoolExecutor 中的一个内部类 , 他是一个包装类 , 是一个线程单元 , 同时提供线程的中断等功能
1 | java复制代码// 问题一 : Worker 结构 |
拒绝策略部分 Policy
ThreadPoolExecutor 中提供了4 个拒绝策略内部类 , 具体的类型详见上文 , 这里来看一下结构 :
1 | java复制代码public interface RejectedExecutionHandler { |
10.2 主要流程
Step 1 : 进入的起点 - 线程的封装
1 | java复制代码// task 的构建 : 匿名传进来的线程会构建成一个 FutureTask |
Step 2 : 运行的起点 - execute
ThreadPoolExecutor 中 excutor 方法是执行的起点 , 其中会进行三种操作
- 当线程池未满时 , 直接 addWorker 运行
- 当线程池满了且正在运行时 , 将线程加入 workQueue 中
- 当上述均失败后 , 就会调用 reject 来处理异常情况 (RejectedExecutionHandler)
1 | java复制代码// 问题一 : execute 中线程池处理任务的逻辑 |
1 | java复制代码// 问题二 : 线程池运行 Work 详情 (简述一下就是核心的四步) |
Step 3 : 工厂的创建
1 | java复制代码// 问题四 : 创建工厂 |
Step 4 : 线程的复用
在上文 Step 1 问题一 中 , 将 线程加入到 workQueue 中了isRunning(c) && workQueue.offer(command)
, 这里就是取出来的步骤 :
1 | java复制代码// 这个问题涉及到的方法主要包括 getTask () |
Step 5 : 拒绝策略
1 | java复制代码// 瞅一瞅拒绝策略 : |
Step 6 : 线程的关闭
1 | java复制代码1 checkShutdownAccess 校验是否可以关闭 |
Step 7 : 如何实现回调 ?
1 | java复制代码submit 回调 |
10.2 底层复杂分析
ctl 到底怎么玩的 ?
1 | java复制代码> private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); |
线程池公式
1 | java复制代码> 计算密集型 :Ncpu + 1 |
比较当前线程容量的方法 workerCountOf(c) 为什么要 & 一个 CAPACITY ?
1 | java复制代码1 > public static final int SIZE = 32; |
十一. 线程池使用
1 | java复制代码 |
十二 . 线程池的想法
1 | java复制代码// 使用线程池时有一些规约和建议是需要注意的 : |
总结
看完并发编程的艺术再来改 ,给我等着 , 还不信搞不定你了
更新记录
- 20210721 : 优化内容 , 重新布局
- 20210820 : 完善细节 , 使内容更流畅
- 20210830 : 优化源码细节和逻辑
本文转载自: 掘金