1. 为什么要用线程池
- 降低资源消耗。通过重复利用已创建的线程降低创建/销毁线程造成的消耗
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就立即执行
- 提高线程的可管理性。线程时稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配/调优和监控。
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后再线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
主要特点:线程复用;控制最大并发数;管理线程。
2. 架构说明
线程池的底层就是ThreadPoolExecutor,Executor 接口
3. ThreadPoolExecutor线程池类参数详解
参数 | 说明 |
corePoolSize | 核心线程数量,线程池维护线程的最少数量 |
maximumPoolSize | 线程池维护线程的最大数量 |
keepAliveTime | 线程池除核心线程外的其他线程的最长空闲时间,超过该时间的空闲线程会被销毁 |
unit | keepAliveTime的单位,TimeUnit中的几个静态属性:NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS |
workQueue | 线程池所使用的任务缓冲队列 |
threadFactory | 线程工厂,用于创建线程,一般用默认的即可 |
handler | 线程池对拒绝任务的处理策略 |
当线程池任务处理不过来的时候(什么时候认为处理不过来后面描述),可以通过handler指定的策略进行处理,ThreadPoolExecutor提供了四种策略:
- ThreadPoolExecutor.AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException异常;阻止系统正常运行,也是默认的处理方式。
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务,”调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新的任务流量。可以通过实现RejectedExecutionHandler接口自定义处理方式。
以上四种拒绝策略均实现了RejectedExecutionHandle接口
拒绝策略:等待队列已经排满了,再也塞不下新任务了,同时线程池种max线程也达到了,无法继续为新任务服务,则会时候我们就需要拒绝策略机制合理的处理这个问题。
4. 线程池任务执行
4.1. 添加执行任务
- submit() 该方法返回一个Future对象,可执行带返回值的线程;或者执行想随时可以取消的线程。Future对象的get()方法获取返回值。Future对象的cancel(true/false)取消任务,未开始或已完成返回false,参数表示是否中断执行中的线程
- execute() 没有返回值。
4.2. 线程池任务提交过程
一个线程提交到线程池的处理流程如下图
- 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
- 如果此时线程池中的数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
- 如果此时线程池中的数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
- 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
- 当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。
总结即:处理任务判断的优先级为 核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。
** 注意:**
- 当workQueue使用的是无界限队列时,maximumPoolSize参数就变的无意义了,比如new LinkedBlockingQueue(),或者new ArrayBlockingQueue(Integer.MAX_VALUE);
- 使用SynchronousQueue队列时由于该队列没有容量的特性,所以不会对任务进行排队,如果线程池中没有空闲线程,会立即创建一个新线程来接收这个任务。maximumPoolSize要设置大一点。
- 核心线程和最大线程数量相等时keepAliveTime无作用.
4.3. 线程池关闭
- shutdown() 不接收新任务,会处理已添加任务
- shutdownNow() 不接受新任务,不处理已添加任务,中断正在处理的任务
5. 常用队列介绍
- ArrayBlockingQueue: 这是一个由数组实现的容量固定的有界阻塞队列.
- SynchronousQueue: 没有容量,不能缓存数据;每个put必须等待一个take; offer()的时候如果没有另一个线程在poll()或者take()的话返回false。
- LinkedBlockingQueue: 这是一个由单链表实现的默认无界的阻塞队列。LinkedBlockingQueue提供了一个可选有界的构造函数,而在未指明容量时,容量默认为Integer.MAX_VALUE。
队列操作:
方法 | 说明 |
add | 增加一个元索; 如果队列已满,则抛出一个异常 |
remove | 移除并返回队列头部的元素; 如果队列为空,则抛出一个异常 |
offer | 添加一个元素并返回true; 如果队列已满,则返回false |
poll | 移除并返回队列头部的元素; 如果队列为空,则返回null |
put | 添加一个元素; 如果队列满,则阻塞 |
take | 移除并返回队列头部的元素; 如果队列为空,则阻塞 |
element | 返回队列头部的元素; 如果队列为空,则抛出一个异常 |
peek | 返回队列头部的元素; 如果队列为空,则返回null |
6. Executors线程工厂类
- Executors.newCachedThreadPool();一池N线程。适用:执行很多短期异步的小程序或者负载较轻的服务
说明: 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程.
内部实现:new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,new SynchronousQueue());
- Executors.newFixedThreadPool(int);固定线程数的池子。执行长期的任务,性能好很多
说明: 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
内部实现:new ThreadPoolExecutor(nThreads, nThreads,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue());
-
Executors.newSingleThreadExecutor();一池一线程,一个任务一个任务执行的场景
说明:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照顺序执行。
内部实现:new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue())
-
Executors.newScheduledThreadPool(int);
说明:创建一个定长线程池,支持定时及周期性任务执行。
内部实现:new ScheduledThreadPoolExecutor(corePoolSize) -
Executors.newWorkStealingPool(int),java8新增,使用目前机器上可用的处理器作为它的并行级别
** 【附】阿里巴巴Java开发手册中对线程池的使用规范 **
2. 【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
正例:
public class TimerTaskThread extends Thread {
public TimerTaskThread(){
super.setName("TimerTaskThread");
...
}
}
- 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明: 使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资
源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者
“过度切换”的问题。 - 【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样
的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明: Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2) CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE, 可能会创建大量的线程,从而导致 OOM。
demo
package com.neu.controller.study;
import java.util.concurrent.*;
/**
* 4种使用多线程的方式
* 1.继承Thread类
* 2.实现Runnable接口(run方法),没有返回值,不抛出异常
* 3.实现Callable接口(call方法),有返回值,抛异常
* 4.通过线程池
*
* 线程池
*
* 第4种获得使用Java多线程的方式,线程池
*/
public class MyThreadPoolDemo {
public static void main(String[] args) {
//知道我们的服务器是几核的
System.out.println(Runtime.getRuntime().availableProcessors());//电脑的线程,就是本机电脑的核数
//一般我们都是用继承ThreadPoolExecutor实现的,而不用jdk自带的
ExecutorService threadPool = new ThreadPoolExecutor(
2,//核心线程数
5,//最大线程数
1L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(3),//任务缓冲队列
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy());//丢弃任务,但是不抛出异常
try{
for (int i=1;i<=10;i++){
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"\t 办理业务");
});
// try{TimeUnit.MILLISECONDS.sleep(200);}catch(Exception e){e.printStackTrace();}
}
}catch(Exception e){
e.printStackTrace();
}finally{
threadPool.shutdown();
}
}
// public static void threadPoolInit(){
// //System.out.println(Runtime.getRuntime().availableProcessors());//电脑的线程
ExecutorService threadPool = Executors.newFixedThreadPool(5);//一池5个处理线程
ExecutorService threadPool = Executors.newSingleThreadExecutor();//一池一个处理线程
// ExecutorService threadPool = Executors.newCachedThreadPool();//一池N个线程
//
// //模拟10个用户来办理业务,每一个用户就是一个来自外部的请求的线程
// try{
// for (int i=1;i<=10;i++){
// threadPool.execute(()->{
// System.out.println(Thread.currentThread().getName()+"\t 办理业务");
// });
try{TimeUnit.MILLISECONDS.sleep(200);}catch(Exception e){e.printStackTrace();}
// }
//
// }catch(Exception e){
// e.printStackTrace();
// }finally{
// threadPool.shutdown();
// }
// }
}
运行结果
8
pool-1-thread-1 办理业务
pool-1-thread-2 办理业务
pool-1-thread-2 办理业务
pool-1-thread-3 办理业务
pool-1-thread-2 办理业务
pool-1-thread-4 办理业务
pool-1-thread-1 办理业务
pool-1-thread-5 办理业务
7. 生产上实际的问题?
问题1,你在工作种单一的/固定的/可变的三种创建线程池的方法,你用哪个多?
一个都不用,我们只用自定义的。
Executors种jdk已经给你提供了,为什么不用?
不用自定义的,我们都是自己去实现线程池,jdk自带的线程使用LinkedBlockingQueue,如果有大量的请求,会导致OOM。
问题2生产上其他配置‘
(1)线程池的4种拒绝策略
(2)自定义线程池 见上面的demo
(3)合理配置线程池你是如何考虑的
配置maxmumPoolSize
【方法一】CPU密集型 最大设置32
System.out.println(Runtime.getRuntime().availableProcessors());//电脑的线程
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程)
而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总的运算能力就那些。
CPU密集型任务配置尽可能少的线程数量:
一般公式:CPU核数+1个线程的线程池
多数公式:由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2
【方法二】Cpu核数/(1-阻塞系数)
IO密集型,即该任务需要大量的IO,即大量的阻塞。
在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。
所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
IO密集型时,大部分线程都阻塞,故需要多配置线程数:
参考公式:CPU核数/(1-阻塞系数) 阻塞系数在0.8~0.9之间
比如8核CPU:8/(1-0.9)=80个线程数
8. 总结
ThreadPoolExecutor通过几个核心参数来定义不同类型的线程池,适用于不同的使用场景;其中在任务提交时,会依次判断corePoolSize, workQueque, 及maximumPoolSize,不同的状态不同的处理。技术领域水太深,如果不是日常使用,基本一段时间后某些知识点就忘的差不多了,因此阶段性地回顾与总结,对夯实自己的技术基础很有必要。