文章目录
一. 理解什么是线程池
线程池就是事先将多个线程对象放到一个容器中,当使用的时候不用 new 线程,而是直到从线程池中拿线程即可。
下边银行的例子
二. 为什么要使用线程池 —— 线程池的好处
线程池最大的好处就是减少每次启动、销毁线程的损耗(记不住的话就记这句话就够了)
- 降低资源损耗 —— 通过重复利用已创建的线程降低线程创建和损耗造成的消耗
- 提高响应速度 —— 任务到达时,可以不需要等到线程创建就能立即执行
- 提高线程的客观理性 —— 使用线程池可以进行统一的分配、调优和监控
三. 线程池的主要参数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BolckingQueue<Runnable> workQueue){
this(corePoolSize, maximunPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler);
}
- corePoolSize(线程池核心线程的大小):向线程池提交一个任务时,如果当前线程池中已创建的线程数小于 corePoolSize,即便此刻存在空闲的线程也会创建一个新线程来执行该任务,直到已创建的线程数大于或等于 corePoolSize 时(除了利用提交新任务来创建和启动线程,也可以用 prestartCoreThread() 或 prestartAllCoreThreads()方法来提前启动线程池中的基本线程)—— 通俗点来说就是,线程池中常驻线程的最大数量。
- maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数,包括核心线程和非核心线程。
- keepAliveTime(线程存活保持时间):线程池中空闲的非核心线程所能存活的最长时间。
- unit(时间单位):与keepAliveTime 搭配使用,是keepAliveTime 的时间单位。
- workQueue(任务队列):用于存放任务的阻塞队列。
- threadFactory(线程工厂):表示生成线程池中工作流程的流程工厂,用户创建新线程。
- defaultHandler(线程饱和策略):线程池饱和策略。
一家银行总共有 6 个窗口(maximumPoolSize),周末开了 3 分窗口办理业务(corePoolSize),某段时间来了 3 个人办理业务,三个窗口能应付过来,这时又来了 1 个,三个窗口便忙不过来了,只好让新来的客户去等待区等待(workQueue),接下来如果还有客户来,就让客户区等待区等待。但是如果等待区也坐满了,业务经理(threadFactory)便通知剩下的窗口开启办理业务,但是如果六个窗口都占满了,此时等待区也坐不下了。这个时候银行便要考虑采用什么方式(defaultHandler)来拒绝客户。过了段时间,只剩下 3 个客户在办理,这时空闲了 3 分新增的窗口,他们便开始等待一定时间(keepAliveTime)(unit),如果时间到了还没有客户来办理业务,这 3 个新增的窗口便可以管理。但是原来的 3 个窗口还继续开着。
四. 线程池执行流程
提交一个新任务:
- 判断线程池中核心线程数是否达到阈值 corePoolSize,若没有达到,则创建一个新核心线程执行任务,若满了,则进行下一步:
- 判断工作队列 workQueue 是否已满,若没满,则将新提交的任务添加到工作队列中,若满了,则进行下一步:
- 判断线程池中线程数目是否已达到阈值 maximumPoolSize,若没有达到,则新建一个工作线程(非核心线程)来执行任务,若满了,则执行饱和策略:
饱和策略:
- AbortPolicy:直接抛出一个异常,默认策略;
- DiscardPolicy:直接丢弃任务
- DiscardOldestPolicy:丢弃队列中等待最久的任务。
- CallerRunsPolicy:主线程中执行任务
五. 线程池为什么需要使用(阻塞)队列?
- 因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM(out of memory,内存用完了),并且会造成 cpu 过度切换。
- 创建线程池的消耗较高
- 线程池创建需要获取 mainlock 这个全局锁,影响并发效率,阻塞队列可以很好的缓冲。(不高明)
还有一个问题:问什么要用阻塞队列而不是非阻塞队列?
阻塞队列可以保证当任务队列中没有任务时,堵塞前来获取任务的线程,使得线程进入 wait 状态,释放 cpu 资源,当队列中有任务时才唤醒对应线程从队列中取出任务并执行。
六. 如何配置线程池
6.1 CPU 密集型任务:
尽量使用较小的线程池,一般为 cpu核心数+1。因为 cpu 密集型任务,使得 cpu 使用效率很高,若开过多的线程数,会造成 cpu 过度切换。
6.2 IO 密集型任务:
可以使用稍大的线程池,一般为 cpu核心数*2。IO密集型任务使得cpu使用效率并不高,因此可以让 cpu 在等待 IO 的时候有其他线程去处理别的任务。
6.3 混合型任务:
可以将任务分为 IO 密集型任务 和 CPU 密集型任务,然后分别用不同的线程去处理。
七. 使用无界阻塞队列的线程池会导致内存飙升吗?
会,FixedThreadPool 使用了无界阻塞队列 LinkedBlockingQueue,如果线程获取了一个任务后,执行的时间比较长,会导致阻塞队列的任务越积越多,从而导致内存使用不停飙升,最终导致 OOM
八. Java中提供的线程池
方法 | corePoolSize | maximumPoolSize | keepAliveTime | workQueue |
---|---|---|---|---|
SingleThreadExecutor | 1 | 1 | 0 | LinkedBlockingQueue |
FixedThreadPool | nThreads | nThreds | 0 | LinkedBlockingQueue |
CachedThreadPool | 0 | Integer.MAX_VALUE | 60s | SynchronousQueue |
ScheduledThreadPool | corePoolSize | Integer.MAX_VALUE | 0 | DelayWorkQueue |
- newSingleThreadExecutor:创建一个单线程的线程池,线程池中只有一个线程在工作,采用无界阻塞队列。来一个执行一个,来多了就在堵塞队列中等待,适用于需要保证顺序执行各个任务,适用于串行执行任务场景。
- newFixedThreadPool:创建一个固定大小的线程池,线程池中只有核心线程,采用无界阻塞队列。线程空闲下来不会被回收。
- newCachedThreadPool:创建一个可以无限扩大的线程池。适用于负载较轻的场景,执行短期异步任务。
- newScheduledThreadPool:适用于执行延时或者周期性任务。
ExecutorService pool = Executors.newFixedThreadPool(10);
九. 运行一个线程池
- execute(),执行一个任务,没有返回值
- submit(),执行一个任务,有返回值
线程池中 submit() 和 execute() 方法有什么区别?
接收参数:execute() 只能执行 Runnable 类型的任务。submit() 可以执行 Runnable 和 Callable 类型的任务;
返回值:submit() 方法可以返回持有计算结果的 Future 对象,而 execute() 没有返回值;
异常处理:submit() 方便 Exception 处理
十. 几种典型的工作队列
- ArrayBlockingQueue:使用数组实现的有界阻塞队列,特性是先进先出
- LinkedBlockingQueue:使用链表实现的无界阻塞队列,特性是先进先出
- DelayQueue:无界阻塞延迟队列,队列中的任务均有过期时间,从队列中获取元素时,只有过期元素才会出队列,队列头时最快要过期的元素。
- SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作,必须要等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。
架构说明
- Executor 接口表示线程池,它的 execute(Runnable task) 方法来执行 Runnable 类型的任务。
- ExecutorService 中声明了管理线程池的方法,比如用于关闭线程池的 shutdown() 方法等
- Executors 类中包含一些金泰方法,他们负责生成各种类型的线程池 ExecutorService 实例。