线程池
前言
池,我们听过有很多,诸如:常量池、数据库连接池、线程池、进程池、内存池…
池的作用就是提高效率。
池的核心思路不外乎两种:
- 提前把要用的对象准备好
- 用完的对象也不要立即释放,留在池子里,以备下次使用
就好像拼乐高积木一样,你会把你常用的一些零件放在一个盒子里,这样拼的时候就非常方便拿取,效率会快很多。这里的 “盒子”,就是 “池”
为什么会有池化技术的出现?
最开始,进程的出现解决了 “并发编程“ 问题。但是由于频繁创建、销毁进程,成本过高,所以引入了 轻量级进程 -> 线程
但是如果创建、销毁线程的频率进一步提高,此时的开销也不能忽略掉
所以为了解决上述问题,有两种解决方案:
- 引入 轻量级线程 -> 纤程(或称 “协程”)
- 协程本质上是程序猿在 用户态代码 中进行调度,而不是靠内核的调度器,这就节省了很多在调度上的开销(操作系统内核中是没有协程的)
- 在用户代码中,协程是基于线程封装出来的,其底层实现有不同方案:
- 可能是N个协程对应一个线程;也可能是N个协程对应M个线程
- 线程池。把要使用的线程提前创建好,用完了也不要直接释放,而是保存,以备下次使用。这就节省了线程创建与销毁的开销
- 为什么从线程池里取线程,就比从系统中申请更高效呢?
打个比方,现在你要去当地某办事处办某个证,你可以选择在小程序上办理,也可以直接去当地政府办事处办理。你在小程序上办理的速度是由你来控制的,就非常的快;但如果你去当地办事处交给工作人员办理,那就不好说什么时候办好了
- 所以说从线程池取线程,就是比从系统中申请更高效
线程池的参数介绍
标准库中提供了一个叫:ThreadPoolExecutor类
这个类的构造方法有很多参数,其参数侧面反映了线程池的设计思路,所以我们要去了解。
其构造方法有四个版本,我们用参数最多的那个版本来讲解(这个版本参数是最全的,了解完这个版本其他的也就都了解了)
-
第一个参数
int corePoolSize
:核心线程数- Java线程池中会 长期保持 这个数量的线程
-
第二个参数
int maximumPoolSize
:最大线程数
标准库提供的线程池,所持有的线程个数,并非是一成不变的,会根据当前任务量,自适应线程个数(任务非常多,就多搞几个线程;任务比较少,就少搞几个线程)
这并不是说第一个参数是 最小线程数。打个比方,第一个参数相当于正式工,第二个参数相当于临时工。任务少的时候开除临时工降低成本,但是正式工是不能随便开除的;任务多时再招几个临时工便可。
-
第三个参数
long keepAliveTime
:存活时间 -
第四个参数
TimeUnit unit
:时间单位(ms,s,min,h…)
这两个参数是一起用的,第一个不要理解错误,这是 “临时工” 线程允许最大的空闲(摸鱼)时间,超过了这个时间阈值,这个线程就会被销毁掉(除核心线程之外多加的那些临时线程) -
第五个参数
BlockingQueue<Runnable> workQueue
:和定时器类似,线程池中也可以池有多个任务,也可以设置成 PriorityBlockingQueue,这就带有优先级- Runnable 就是描述这个任务,这个之前已经写过很多遍了,不再赘述
-
第六个参数
ThreadFactory threadFactory
:线程工厂- 线程工厂,就相当于在这个类里面提供了方法(当然,也不一定非得是静态方法),让这个方法封装了
new Thread
这个操作,并且同时给 Thread 设置一些属性。此时就构成了 ThreadFactory - 总结:这个线程工厂就是用来创建线程的。工厂,就是以流水线的方式生产东西。
- 线程工厂,就相当于在这个类里面提供了方法(当然,也不一定非得是静态方法),让这个方法封装了
-
第七个参数[重要]
RejectedExecutionHandler handler
:拒绝策略- 在线程池中,有一个阻塞队列,其能够容纳的元素是有 上限 的。当任务队列已经满了,如果继续往队列中添加任务,那么线程池会怎么办?
有以下四个策略:
- 如果继续添加任务,那么直接就抛出异常(旧任务和新任务,爷都不干了~直接撂挑子)
- 新的任务,由添加任务的线程负责执行(谁揽的活谁去干,反正爷不去)
- 丢弃最老的任务,添加新的任务(这个好理解,我能干10个活,我把我手中最老的那个任务扔了,正好空出一个任务容量, 把这个任务添加进来)
- 丢弃最新的任务(谁也别干了,爷不干,把这个新任务添加进来的那个线程也不干,都不干~)直接把这个新的任务扔掉
因为线程池过于复杂,于是Java标准库提供了更便捷的Executor类
ThreadPoolExecutor 本身比较复杂,因此标准库还提供了另一个版本,把这个类封装了一下,称为 Executors
通过这个类来创建出不同的线程池对象(在内部把 ThreadPoolExecutor 创建好了,并且设置了不同的参数)
演示:
其核心方法就是 submit()
线程池的工作过程(模拟线程池)
模拟一个 固定线程数目的线程池(暂时不考虑线程的增加和减少)
思路分为以下四步:
- 提供构造方法,指定创建出多少个线程
- 在构造方法中,把这些线程都创建好
- 提供一个阻塞队列,能够持有要执行的任务
- 提供
submit()
方法,可以添加新的任务