1、为什么会有线程池
JVM中的一个线程即对应一个操作系统的线程,也就是JVM的线程是由操作系统创建而来,创建线程和销毁线程这些都需要操作系统来分别赋予资源和释放资源等
也就意味着创建线程变成了一个比较重的操作
我们可以利用多线程去进行不同的工作,更高效的利用CPU资源,但是这并不意味着线程数量越多越好
我们的时代已经由原来的单核时代变成现在的多核时代了,这个核指的就是CPU,在原来的单核时代,如果一个线程一直是运算的逻辑过程,也就不涉及到线程的切换,因为这个线程一直在占用CPU,也就是属于计算密集型
但是如果这个线程属于IO密集型,也就是这个线程很多的时间都是在等待IO操作和处理IO操作,这样就浪费了CPU这个大脑的处理能力了
于是就有了多线程,一个线程等待IO操作,另一个线程可以顶上,充分利用了CPU的资源
随着多核时代的到来,对于这个CPU高效利用也就变得更加迫切,CPU的核心越来越多,能同时运行的线程数越来越多了,也就意味着此时的多线程并不只是去提高单核的处理能力,更是为了充分利用这个多核的大脑
但 CPU 的核心数有限,同时能运行的线程数有限,所以需要根据调度算法切换执行的线程,而线程的切换需要开销,比如替换寄存器的内容、高速缓存的失效等等。
如果线程数太多,切换的频率就变高,可能使得多线程带来的好处抵不过线程切换带来的开销,得不偿失。
因此线程的数量需要得以控制
2、什么是线程池
线程的数量太少无法充分利用CPU,线程数太多的话会导致频繁切换线程,上下文切换消耗资源,我们需要根据系统资源和业务性能来决定线程数量
而线程的创建又是属于一个比较重的操作,所以我们想到的就是缓存一批线程,这种思想大家都明白应该,就像是数据库某张表需要经常查询,造成DB压力过大,我们就先把经常访问访问的数据放入到缓存中,用于缓解对于DB的访问压力
这个也是类似的道理,每次去新建和销毁线程比较重,我们就可以通过缓存这些线程来减轻不必要的消耗
线程的数量我们需要根据硬件的资源和线程要执行的任务这些等综合来决定
高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
并发不高、任务执行时间长的业务要分情况来讨论
假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的
CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务
假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,线程数设置为CPU核数+1,线程池中的线程数设置得少一些,减少线程上下文的切换
并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,参考上面的设置即可。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。
大家应该都听过对象池、连接池这些,池化的技术就是通过在池子里取出资源,然是使用完再放回到池子里,而线程池这一点稍微不太一样,这里线程池相对来说更黑盒一些
不是我们从线程池中取线程使用,而是直接往线程池里扔任务,然后线程池帮我们去执行
3、实现线程池
线程池内部也是一个典型的生产者-消费者模型
线程池内部有一个存放任务列表的队列,而内部会不断的有线程去队列中取任务来执行,用来消费
来看一个简易版的线程池实现,这段代码同样来源于上面的博文
首先线程池内需要定义两个成员变量,分别是阻塞队列和线程列表,然后自定义线程使它的任务就是不断的从阻塞队列中拿任务然后执行。
@Slf4j
public class YesThreadPool {
BlockingQueue<Runnable> taskQueue; //存放任务的阻塞队列
List<YesThread> threads; //线程列表
YesThreadPool(BlockingQueue<Runnable> taskQueue, int threadSize) {
this.taskQueue = taskQueue;
threads = new ArrayList<>(threadSize);
// 初始化线程,并定义名称
IntStream.rangeClosed(1, threadSize).