本文主要来介绍一下其中比较简单的线程池的实现原理,但愿读者们能够触类旁通,经过对线程池的理解,学习并掌握全部编程中池化技术的底层原理。java
建立一个线程编程
在Java的并发编程中,线程是十分重要的,在Java中,建立一个线程比较简单:安全
public class App { public static void main(String[] args) throws Exception { new Thread(new Runnable() { @Override public void run() { System.out.println("线程运行中"); } }).start(); } }
咱们经过建立一个线程对象,而且实现Runnable接口就能够实现一个简单的线程。能够利用上多核CPU。当一个任务结束,当前线程就接收。性能优化
但不少时候,咱们不止会执行一个任务。 若是每次都是如此的建立线程->执行任务->销毁线程,会形成很大的性能开销。架构
那可否一个线程建立后,执行完一个任务后,又去执行另外一个任务,而不是销毁。这就是线程池。并发
这也就是池化技术的思想,经过预先建立好多个线程,放在池中,这样能够在须要使用线程的时候直接获取,避免屡次重复建立、销毁带来的开销。分布式
线程池的简单使用ide
如下代码,是在Java中建立线程池:函数
import java.util.concurrent.*; public class App { public static void main(String[] args) throws Exception { ExecutorService executorService = new ThreadPoolExecutor(1, 1, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10)); executorService.execute(new Runnable() { @Override public void run() { System.out.println("abcdefg"); } }); executorService.shutdown(); } }
Jdk提供给外部的接口也很简单。直接调用ThreadPoolExecutor构造一个就能够了,也能够经过Executors静态工厂构建,但通常不建议。微服务
能够看到,开发者想要在代码中使用线程池仍是比较简单的,这得益于Java给咱们封装好的一系列API。可是,这些API的背后是什么呢,让咱们来揭开这个迷雾,看清线程池的本质。
线程池构造函数
一般,通常构造函数会反映出这个工具或这个对象的数据存储结构。
若是把线程池比做一个公司。公司会有正式员工处理正常业务,若是工做量大的话,会雇佣外包人员来工做。
闲时就能够释放外包人员以减小公司管理开销。一个公司由于成本关系,雇佣的人员始终是有最大数。
若是这时候还有任务处理不过来,就走需求池排任务。
acc : 获取调用上下文
corePoolSize: 核心线程数量,能够类比正式员工数量,常驻线程数量。
maximumPoolSize: 最大的线程数量,公司最多雇佣员工数量。常驻+临时线程数量。
workQueue:多余任务等待队列,再多的人都处理不过来了,须要等着,在这个地方等。
keepAliveTime:非核心线程空闲时间,就是外包人员等了多久,若是尚未活干,解雇了。
threadFactory: 建立线程的工厂,在这个地方能够统一处理建立的线程的属性。每一个公司对员工的要求不同,恩,在这里设置员工的属性。
handler:线程池拒绝策略,什么意思呢?就是当任务实在是太多,人也不够,需求池也排满了,还有任务咋办?默认是不处理,抛出异常告诉任务提交者,我这忙不过来了。
添加一个任务
接着,咱们看一下线程池中比较重要的execute方法,该方法用于向线程池中添加一个任务。
核心模块用红框标记了。
第一个红框: workerCountOf方法根据ctl的低29位,获得线程池的当前线程数,若是线程数小于corePoolSize,则执行addWorker方法建立新的线程执行任务;
第二个红框:判断线程池是否在运行,若是在,任务队列是否容许插入,插入成功再次验证线程池是否运行,若是不在运行,移除插入的任务,而后抛出拒绝策略。若是在运行,没有线程了,就启用一个线程。
第三个红框:若是添加非核心线程失败,就直接拒绝了。
这里逻辑稍微有点复杂,画了个流程图仅供参考
接下来,咱们看看如何添加一个工做线程的?
添加worker线程
从方法execute的实现能够看出:addWorker主要负责建立新的线程并执行任务,代码以下(这里代码有点长,不要紧,也是分块的,总共有5个关键的代码块):
第一个红框:作是否可以添加工做线程条件过滤:
判断线程池的状态,若是线程池的状态值大于或等SHUTDOWN,则不处理提交的任务,直接返回;
第二个红框:作自旋,更新建立线程数量:
经过参数core判断当前须要建立的线程是否为核心线程,若是core为true,且当前线程数小于corePoolSize,则跳出循环,开始建立新的线程
有人或许会疑问 retry 是什么?这个是java中的goto语法。只能运用在break和continue后面。
接着看后面的代码:
第一个红框:获取线程池主锁。
线程池的工做线程经过Woker类实现,经过ReentrantLock锁保证线程安全。
第二个红框:添加线程到workers中(线程池中)。
第三个红框:启动新建的线程。
接下来,咱们看看workers是什么。
一个hashSet。因此,线程池底层的存储结构其实就是一个HashSet。
worker线程处理队列任务
第一个红框:是不是第一次执行任务,或者从队列中能够获取到任务。
第二个红框:获取到任务后,执行任务开始前操做钩子。
第三个红框:执行任务。
第四个红框:执行任务后钩子。
这两个钩子(beforeExecute,afterExecute)容许咱们本身继承线程池,作任务执行先后处理。
到这里,源代码分析到此为止。接下来作一下简单的总结。
总结
所谓线程池本质是一个hashSet。多余的任务会放在阻塞队列中。
只有当阻塞队列满了后,才会触发非核心线程的建立。因此非核心线程只是临时过来打杂的。直到空闲了,而后本身关闭了。
线程池提供了两个钩子(beforeExecute,afterExecute)给咱们,咱们继承线程池,在执行任务先后作一些事情。
线程池原理关键技术:锁(lock,cas)、阻塞队列、hashSet(资源池)
在此我向你们推荐一个架构学习交流群。交流学习群号:821169538 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多。
最后但愿对你理解线程池有帮助。最后,留一个思考题,为何线程池的底层数据接口采用HashSet来实现?