1. 简述
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务加入队列,然后在线程创建后启动这些任务,如果线程池超过了最大数量,超出的数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行;
他的主要特点为:
- 线程复用:
降低资源消耗.通过重复利用自己创建的线程降低线程创建和销毁造成的消耗. - 控制最大并发数:
提高响应速度.当任务到达时,任务可以不需要等到线程就能立即执行. - 管理线程
提高线程的可管理性.线程是稀缺资源,如果无限的创线程,不仅会消耗资源,还会较低系统的稳定性,使用线程池可以进行统一分配,调优和监控.
2. 框架实现
Java中的线程池是通过Executor
框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类.
3. 线程池的五大种类
线程池的核心类是 ThreadPoolExecutor
3.1 newScheduledThreadPool
带时间调度的,多少时间执行一次
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
3.2 newWorkStealingPool
jdk8新推出的,使用目前机器上可用的处理器作为他的并行级别
ExecutorService executorService = Executors.newWorkStealingPool()
3.3 newFixedThreadPool
固定线程数
ExecutorService threadPool = Executors.newFixedThreadPool(5);
3.4 newCachedThreadPool
带缓冲的线程池
ExecutorService threadPool3 = Executors.newCachedThreadPool();
3.5 newSingleThreadExecutor
单个线程的线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();
4. 七大参数
4.1 int corePoolSize
线程池当中的核心数,相当于银行里面当日值班窗口2个,虽然一共有5个窗口
4.2 int maximumPoolSize
线程池当中最大的核心数,也就是5个窗口,当corePoolSize不够用了且队列满了,才会慢慢扩大,直到达到最大值
4.3 long keepAliveTime
多余的空闲线程的存活时间,表示当前线程数大于corePoolSized时,当空闲时间达到KeepAliveTime时,多余的线程会被销毁只剩下corePoolSizeg个线程
4.4 TimeUnit unit
keepAaliveTime的时间单位
4.5 BlockingQueue workQueue
任务队列,相当于好比你去银行办理业务,但是窗口满了,只能取号在候客区排队等候
4.6 ThreadFactory threadFactory
生成线程池当中工作线程的线程工厂,用于创建线程一般用默认的即可。相当于每家银行都有大堂经理等员工,属于标配。
4.7 RejectedExecutionHandler handler
拒绝策略,表示当队列满了且工作线程数大于线程池最大数
5. 四大拒绝策略
jdk内置的四种拒绝策略, 以下内置策略均实现了RejectExecutionHandler接口
5.1 AbortPolicy(默认)
直接抛出RejectExecutionException异常阻止系统正常运行。为java线程池默认的阻塞策略。切记会中断调用者的处理过程,因此需要try catch,否则程序会直接退出。
5.2 CallerRunsPolicy
返回给调用者
5.3 DiscardOldestPolicy
丢弃队列最前面(最旧)的任务,然后重新尝试执行任务(重复此过程);
5.4 DiscardPolicy
直接静悄悄的丢弃这个任务,不触发任何动作
6. 底层原理
6.1 底层原理流程
- 1.在创建了线程池后,等待提交过来的任务请求。
- 2.当调用 execute()方法添加一个请求任务时,线程池会做如下判断:
- 2.1 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
- 2.2 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
- 2.3 如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
- 2.4 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
- 3.当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 4.当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
7.问题点
7.1 为什么要使用线程池
【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决
资源不足的问题。如果不使用线程池,可能造成系统创建大量同类线程而导致消耗完内存或
者“过度切换”的问题。
你在实际中具体使用哪种线程池,没自定义过线程池,记住,这里是一个大坑
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样
的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1.FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,(因为 它的底层是ThreadPoolExecutor+LinkedBlockingQueue,而LinkedBlockingQueue是长度是21亿多,相当于无界了)可能会堆积大量的请求,从而导致 OOM。
2.CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
7.2 如何合理配置线程数
Runtime.getRuntime().availableProcessors() 获取核数
分cpu密集型和IO密集型
cpu密集的意思是该任务需要大量的运算,而没有阻塞,cpu一直全速运行。
cpu密集的任务只在真正的多核cpu才可能得到加速
CPU密集型任务配置尽可能少的线程数量:
一般公式: CPU核心数+1个线程的线程池。
IO密集型:分2种: 第一种:由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如 CPU核心数*2
第二种:
IO密集型,即该任务需要大量的IO,即大量的阻塞,在单线程运行IO密集型任务时会导致浪费大量的CPU运算能力在等待上面,所以在IO密集型任务上使用多线程可以大大提高运行速度,即利用了浪费的阻塞时间。
参考公式: CPU核心数/1-阻塞系数 阻塞系统一般在0.8-0.9之间
比如8核CPU 8/(1-0.9)=80个线程
7.2.1 根据实际情况配置核心线程数
假设现在某个接口的P99耗时是200ms, QPS是60,假设是Cpu计算逻辑,应该配置线程池的核心线程数是多少?
200ms*60 /1000ms=12, 也就是说1s之内,有12笔请求需要处理,加上Cpu切换消耗,加上一些负载因子,配置15个差不多,实际多少根据压测效果决定;
更多信息可以参考这篇:https://zhuanlan.zhihu.com/p/634981751
7.3 工作中使用哪个线程池
参考阿里巴巴java开发手册
【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。说明:Executors返回的线程池对象的弊端如下:
1FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
2CachedThreadPool和ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
7.4 自定义线程池
public class MyThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
1L,
TimeUnit.SECONDS,
new LinkedBlockingDeque<Runnable>(3),
Executors.defaultThreadFactory(),
//默认抛出异常
//new ThreadPoolExecutor.AbortPolicy()
//回退调用者
//new ThreadPoolExecutor.CallerRunsPolicy()
//处理不来的不处理
//new ThreadPoolExecutor.DiscardOldestPolicy()
new ThreadPoolExecutor.DiscardPolicy()
);
//模拟10个用户来办理业务 没用户就是来自外部的请求线程.
try {
for (int i = 1; i <= 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
//threadPoolInit();
}
private static void threadPoolInit() {
/**
* 一池5个处理线程
*/
//ExecutorService threadPool= Executors.newFixedThreadPool(5);
/**
* 一池一线程
*/
//ExecutorService threadPool= Executors.newSingleThreadExecutor();
/**
* 一池N线程
*/
ExecutorService threadPool = Executors.newCachedThreadPool();
//模拟10个用户来办理业务 没用户就是来自外部的请求线程.
try {
for (int i = 1; i <= 20; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 办理业务");
});
try {
TimeUnit.MICROSECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
7.5 如何优雅的关闭线程池?
01 线程中断
在介绍线程池关闭之前,先介绍下Thread的interrupt。
在程序中,我们是不能随便中断一个线程的,因为这是极其不安全的操作,我们无法知道这个线程正运行在什么状态,它可能持有某把锁,强行中断可能导致锁不能释放的问题;或者线程可能在操作数据库,强行中断导致数据不一致混乱的问题。正因此,JAVA里将Thread的stop方法设置为过时,以禁止大家使用。
一个线程什么时候可以退出呢?当然只有线程自己才能知道。
所以我们这里要说的Thread的interrrupt方法,本质不是用来中断一个线程。是将线程设置一个中断状态。
当我们调用线程的interrupt方法,它有两个作用:
1、如果此线程处于阻塞状态(比如调用了wait方法,io等待),则会立马退出阻塞,并抛出InterruptedException异常,线程就可以通过捕获InterruptedException来做一定的处理,然后让线程退出。
2、如果此线程正处于运行之中,则线程不受任何影响,继续运行,仅仅是线程的中断标记被设置为true。所以线程要在适当的位置通过调用isInterrupted方法来查看自己是否被中断,并做退出操作。
注:
如果线程的interrupt方法先被调用,然后线程调用阻塞方法进入阻塞状态,InterruptedException异常依旧会抛出。
如果线程捕获InterruptedException异常后,继续调用阻塞方法,将不再触发InterruptedException异常。
02 线程池的关闭
线程池提供了两个关闭方法,shutdownNow
和shuwdown
方法。
shutdownNow方法的解释是:线程池拒接收新提交的任务,同时立马关闭线程池,线程池里的任务不再执行。
shutdown方法的解释是:线程池拒接收新提交的任务,同时等待线程池里的任务执行完毕后关闭线程池。
需要强调一点的是,调用完shutdownNow和shuwdown方法后,并不代表线程池已经完成关闭操作,它只是异步的通知线程池进行关闭处理。如果要同步等待线程池彻底关闭后才继续往下执行,需要调用awaitTermination方法进行同步等待。
shutdownNow方法的源码:
在shutdownNow方法里,重要的三句代码我用红色数字标出来了。
第一句就是原子性的修改线程池的状态为STOP状态
第三句是将队列里还没有执行的任务放到列表里,返回给调用方。
第二句是遍历线程池里的所有工作线程,然后调用线程的interrupt方法。如下图:
以上就是shutdownNow方法的执行逻辑:将线程池状态修改为STOP,然后调用线程池里的所有线程的interrupt方法。
shutdown方法的源码:
跟shutdownNow类似,只不过它是将线程池的状态修改为SHUTDOWN状态,然后调用interruptIdleWorkers方法,来中断空闲的线程。
总结:
当我们调用线程池的shuwdown方法时,
如果线程正在执行线程池里的任务,即便任务处于阻塞状态,线程也不会被中断,而是继续执行。
如果线程池阻塞等待从队列里读取任务,则会被唤醒,但是会继续判断队列是否为空,如果不为空会继续从队列里读取任务,为空则线程退出。
03 优雅的关闭线程池
使用shutdownNow方法,可能会引起报错,使用shutdown方法可能会导致线程关闭不了。
所以当我们使用shutdownNow方法关闭线程池时,一定要对任务里进行异常捕获。
使用shuwdown方法关闭线程池时,一定要确保任务里不会有永久阻塞等待的逻辑,否则线程池就关闭不了。
最后,一定要记得,shutdownNow和shuwdown调用完,线程池并不是立马就关闭了,要想等待线程池关闭,还需调用awaitTermination方法来阻塞等待。
7.6 核心线程会被销毁吗?
答案是,可能会;
- 当run方法里面出现异常时
会销毁线程,然后重新创建一个新的放回工作队列
public class TestThreadPool {
public static void main(String[] args) throws InterruptedException {
int coreThread = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor pool = null;
try {
pool = new ThreadPoolExecutor(coreThread, coreThread * 2,
5L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadFactoryBuilder().setNameFormat("test-thread-").build(),
new ThreadPoolExecutor.AbortPolicy());
} catch (Exception e) {
//todo 当队列满了以及最大核心数满了,则会触发 AbortPolicy() 策略,抛异常
//所以需要做好时刻回滚机制,比如在执行任务前,将任务写入本地缓存,线程执行成功,则删掉该记录,执行失败查询缓存继续跑
//可用定时任务做,防止系统宕机
e.printStackTrace();
}
// pool.allowCoreThreadTimeOut(true);
int t1 = 10;
for (int i = 1; i <= t1; i++) {
int t2 = i;
ThreadPoolExecutor finalPool = pool;
pool.submit(new Runnable() {
@Override
public void run() {
if(t2 == 10){
throw new RuntimeException();
}
String name = Thread.currentThread().getName();
//获取活跃的线程数
int activeCount = finalPool.getActiveCount();
//返回当前在线程池的数量
int poolSize = finalPool.getPoolSize();
System.out.println("任务:"+t2+"-,线程名称:"+name+"-活跃线程数:"+activeCount+"-当前线程池线程数"+ poolSize);
}
});
}
Thread.sleep(6000);
System.out.println("当前线程池线程数"+pool.getPoolSize()+"-----,活跃线程数"+pool.getActiveCount());
}
}
开启pool.allowCoreThreadTimeOut(true);
加上 这行代码: pool.allowCoreThreadTimeOut(true);
当allowCoreThreadTimeOut手动设置为true,会销毁线程;
区别:
1. 如果线程在run执行期间抛错,会销毁该线程然后重新创建一个放回去;
2. 如果设置为true,当线程过了超时时间,会被销毁;
7.7 submit和execute区别
7.7.1. 使用submit
- 使用Runnable接口:
不抛异常:
for (int i = 0; i < 10; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
throw new RuntimeException();
}
});
}
System.out.println("----");
执行结果:
抛异常:
总结: 使用runnable接口的时候一定要记得get操作,因为异常被它内部catch了
7.7.2. 使用execute
for (int i = 0; i < 10; i++) {
pool.execute(new Runnable() {
@Override
public void run() {
throw new RuntimeException();
}
});
}
小总结:
execute操作无返回值,会抛异常;
submit()操作有返回值,不会抛异常,需要get才能抛;
7.8 Runnable和Callable区别
使用的时候注意:
Runnable和Callable区别:
- 相同点:
- 都是接口
- 都可以编写多线程程序
- 都采用Thread.start()启动线程
- 不同点:
- callable的核心是call方法,允许返回值,runnable的核心是run方法,没有返回值
- call方法可以抛出异常,但是run方法不行
- 因为runnable是java1.1就有了,所以他不存在返回值,后期在java1.5进行了优化,就出现了callable,就有了返回值和抛异常
- callable和runnable都可以应用于executors。而thread类只支持runnable
在submit操作时,使用runable接口,是把对象丢进去,里面赋值,而callable接口时返回指定类型值
使用背景:
多线程返回执行结果是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而Callable+Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务;
7.8.1 Future接口玩法
Future则是对于具体的Runnable或Callable任务的执行结果进行取消、判断是否完成、获取任务执行结果。
Future接口声明了5个方法:
-
boolean cancel(boolean mayInterruptIfRunning);
尝试取消此任务的执行。如果取消成功则返回true,取消失败则返回false。
如果任务已完成、已取消或由于其他原因无法取消,则此尝试将失败,返回false。
传入的参数如果为true,则直接中断正在执行的任务,返回true;
如果为false,则允许正在进行的任务完成。 -
boolean isCancelled();
任务是否被取消成功,如果此任务在正常工作之前被取消,则返回true。 -
boolean isDone();
如果此任务已完成,则返回true。完成可能是由于正常终止、异常或取消 - 在所有这些情况下,此方法将返回 true。 -
V get() throws InterruptedException, ExecutionException;
以阻塞的方式获取任务执行结果,知道任务执行完毕返回。
另外在以下情况会抛出异常:
-
任务被取消:
抛出 CancellationException 异常 -
计算引发异常:
抛出 ExecutionException 异常 -
当前线程被中断:
抛出 InterruptedException 异常 -
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
同上,但规定了超时时间,如果在规定时间内没有计算出结果,会抛出TimeoutException异常。
玩法:
public class TaskDemo implements Callable<Integer> {
private int sum;
@Override
public Integer call() throws Exception {
System.out.println("Callable线程开始执行任务");
Thread.sleep(1500);
for (int i = 0; i < 50; i++) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("Callable线程计算任务中断");
return null;
}
if (i % 2 == 0) {
sum = sum + i;
System.out.println("sum=" + sum);
}
}
System.out.println("Callable线程执行完毕 计算结果为" + sum);
return sum;
}
}
public class TestThreadPool {
public static void main(String[] args) throws InterruptedException, ExecutionException {
TaskDemo task = new TaskDemo();
int coreThread = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor pool = null;
try {
pool = new ThreadPoolExecutor(coreThread, coreThread * 2,
5L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadFactoryBuilder().setNameFormat("test-thread-").build(),
new ThreadPoolExecutor.AbortPolicy());
} catch (Exception e) {
//todo 当队列满了以及最大核心数满了,则会触发 AbortPolicy() 策略,抛异常
//所以需要做好时刻回滚机制,比如在执行任务前,将任务写入本地缓存,线程执行成功,则删掉该记录,执行失败查询缓存继续跑
//可用定时任务做,防止系统宕机
e.printStackTrace();
}
Future<Integer> future = pool.submit(task);
pool.shutdown();
try {
Thread.sleep(3000L);
System.out.println("主线程执行其他任务中.....");
if (future.get() != null) {
System.out.println("主线程获取Callable执行结果:" + future.get());
} else {
System.out.println("尚未获取到结果");
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
7.8.2 FutureTask
FutureTask类实现了RunnableFuture接口,而RunnableFuture继承了Runnable和Future接口,可知FutureTask可做Runnable和Future一样的事情。
8. 实战:如何根据接口的QPS和P99设定参数
线程池设置的数量和系统的QPS是紧密关联的,比如说我有10个节点,10个节点指的是10个容器,配置都是一样的。然后通过负载均衡进行转发。这样就相当于它平摊了我们的业务流量。也就是平摊了我们QPS的需求。此时有个A接口,他的QPS是2000,P99均值是30ms,那么其实平摊到每个pod节点的QPS大概是 200QPS;那么重点来了,线程池的数量如何设定?
针对上面的场景,200qps 乘以 30毫秒。200QPS0.03=6+ breathing room=10 threads 也就是差不多10个线程数左右,breathing room是冗余量的意思,因为线程的切换和处理,它本身是有业务开销的,以及有一些不确定的情况,可能导致一些线程的浪费,在这种情况下6个线程不足以满足我们的要求,所以这个时候它需要加一些冗余量,冗余一般在0.3到0.5之间; 这个就是你的核心线程数,这个时候结合是否是Cpu密集型还是 IO密集型,如果是Cpu密集型则最大核心线程数2即可,如果是IO密集型*9差不多就可以;