线程池ThreadPoolExecutor详解
线程池是什么
线程池是对一组线程的管理和运用。如果不运用线程池来管理我们的线程,那么我们自己频繁的new线程池会影响程序运行的效率,首先一点,Thread也是一个对象,只要是对象
就有可能会被垃圾回收,频繁回收会带来系统卡顿。而且在实际业务中,我们并不想随意丢弃Thread对象,所以这时候,线程池技术就应运而生了。
线程池的基本用法
略。。。。
线程池执行任务的具体流程是怎样的
线程池提供了两种方法执行任务
void execute(Runnable task)
Future<?> submit(Runnable task)
实际上submit调用的还是execute方法,只不过会返回一个Future对象,用来获取任务执行的结果
1 public <T> Future<T> submit(Callable<T> task) {
2 if (task == null) throw new NullPointerException();
3 RunnableFuture<T> ftask = newTaskFor(task);
4 execute(ftask);
5 return ftask;
6 }
execute方法执行会分为三步,用个流程图就可以看懂了
注意:
1.提交一个Runnable时候,不管当前线程池中的线程是否空闲,只要数量小于核心线程数就会创建新线程
2.线程池相当于是非公平的,比如队列满了之后提交的任务可能会比正在排队的任务先执行(看流程图就懂了)
线程池的五种状态
1.RUNNING: 会接受新任务并且会处理队列中的任务
2.SHUTDOWN: 不会接收新任务并且会处理队列中的任务
3.STOP: 不会接收新任务,并且不会处理队列中的任务,并且会中断在处理的任务(能不能被中断得看任务本身)
4.TIDYING: 所有任务都终止了,线程池中也没有线程了,这样线程池的状态就会转为TIDYING,一旦达到此状态,就会调用线程池的terminated方法
5.TERMINATED: teminated方法执行完后机会转变为 TERMINATED
这五种状态并不能任意转换,只会有以下几种情况:
1.RUNNING->SHUTDOWN:手动调用线程池的shutdown()方法,或者线程池对象GC时会调用finalize()从而调用shutdown()
2.RUNNING、SHUTDOWN->STOP.调用shutdownNow()触发,如果先调shutdown()然后调用shutdownNow(),就会发生SHUTDOWN->STOP
3.SHUTDOWN->TIDYING: 队列为空,并且线程池中没有线程时自动转换
4.STOP->TIDYING: 线程池中没有线程时自动转换(队列中可能还有任务)
5.TIDYING->TERMINATED: terminated方法执行完后
线程池中的线程时如何关闭的
线程想要关闭或则说消亡,要么是任务结束自然消亡,要么是调用线程的中断方法,但是中断方法并不会立即终止线程,而是设置一个标志位
通过标志位然后判断进行线程的关闭。我们来个demo演示下interrupt方法
1 public class InterruptedTest {
2
3 private static int num = 0;
4
5 public static void main(String[] args) throws Exception {
6 Thread t1 = new Thread(() -> {
7 while (true) {
8 if (Thread.currentThread().isInterrupted()) {
9 System.out.println("线程被中断了");
10 //这样线程就自然销毁了
11 break;
12 }
13 if (num++ == 10000) {
14 Thread.currentThread().interrupt();
15 System.out.println("num = " + num);
16 }
17 }
18 });
19 t1.start();
20
21 //保证t1执行完毕
22 t1.join();
23 }
24 }
线程池就是使用这种中断机制使线程消亡的
线程池中为什么要使用阻塞队列
原因有很多。线程池中的线程在运行过程中,执行完创建线程时绑定的第一个任务后,就会不断地从队里中获取任务并执行,那么如果队列中没有任务了
线程为了不自然消亡,就会阻塞在获取队列任务时,等着队列中有任务过来就会拿到任务从而去执行任务。
通过这种方法最终能确保线程池中的核心线程存活,关键代码:
getTask():
Runnable r = timed ?
//poll会在指定的超时时间获取不到任务后返回null,那么没有任务线程就自然销毁了
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
//核心线程就是阻塞在这里,因为take方法一直会阻塞
workQueue.take();
线程池中的线程执行任务发生异常会被移除线程池吗
会,但是如果所有的线程在执行任务时都出错了,会不会导致线程池中没有任何线程了?
在源码中,当执行任务出现异常时候,最终会执行processWorkerExit(),执行完这个方法后,当前线程也就自然消亡了,但是这个方法会额外再新增一个线程,这样
就能维持住固定的核心线程数。
那么现在我们扩展一个问题,线程池在执行任务过程中发生了异常,我们该如何处理呢?
处理的方式很多,比如在任务代码快中用try...catch来捕获异常,然后进行处理,还有种方式就是使用线程工厂来处理,我们来看一段代码:
public class ThreadPoolExceptionAware {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 20, 100,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), new MyThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
executor.execute(() -> {
int i = 1 / 0;
System.out.println("hello");
});
}
}
class MyThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
//设置线程的异常回调
thread.setUncaughtExceptionHandler((t, e) -> {
System.out.println(t.getName() + "线程执行任务发生异常,异常信息为:" + e.getMessage());
});
return thread;
}
}
怎么设置线程池参数
对于这个问题,网上有各种各样的公式,还分CPU密集型和io密集型任务来设置不同的参数。
我的想法很简单,你的业务并发量确实过大,那么核心线程数和最大线程池数都可以设置大一点,核心线程数可以设置成当前计算机的线程数*2,最大线程数可以设置更大
。核心线程我们稍微设置的小一点,因为一般来讲核心线程我们不会消亡。那么并发量过大,我们也还要把阻塞队列设置的大一点,万一线程忙不过来,队列还可以顶一会。
那么对于超时时间,如果核心线程数设置的比较大,我们可以将超时时间设置的小一点,让非核心线程尽快消亡减少系统资源的压力。
具体数值到底该设置多少,最好还是要进行压测进行调整。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?