原文网址:Java线程池--流程/原理/机制--使用步骤/大小设置/饱和策略/拒绝策略/异常处理_IT利刃出鞘的博客-CSDN博客
简介
为什么要用线程池
我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?
在Java中可以通过线程池来达到这样的效果。在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池。
网络请求通常有两种形式
第一种:请求不频繁,且每次连接后会保持相当一段时间来读数据或者写数据,最后断开,如文件下载,网络流媒体等。
另一种:请求频繁,但是连接上以后读/写很少量的数据就断开连接。考虑到服务的并发问题,如果每个请求来到以后服务都为它启动一个线程,那么这对服务的资源可能会造成很大的浪费,特别是第二种情况。
因为通常情况下,创建线程是需要一定的耗时的,设这个时间为T1,而连接后读/写服务的时间为T2,当T1>>T2时,我们就应当考虑一种策略或者机制来控制,使得服务对于第二种请求方式也能在较低的功耗下完成。
通常,我们可以用线程池来解决这个问题,首先,在服务启动的时候,我们可以启动好几个线程,并用一个容器(如线程池)来管理这些线程。当请求到来时,可以从池中取一个线程出来,执行任务(通常是对请求的响应),当任务结束后,再将这个线程放入池中备用;如果请求到来而池中没有空闲的线程,该请求需要排队等候。最后,当服务关闭时销毁该池即可。
线程池作用
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池缺点
- 不能对于线程池中任务设置优先级。
- 适用于生存周期较短的的任务,不适用于又长又大的任务。
- 不能标识线程的各个状态,比如启动线程,终止线程。
- 对于任意给定的应用程序域,只能允许一个线程池与之对应。
- 线程池所有线程都处于多线程单元中,如果想把线程放到单线程单元中,线程池就废掉了。
线程池的组成结构
一个线程池包括四个基本部分
- 线程管理池(ThreadPool):用于创建并管理线程池,有创建,销毁,添加新任务;
- 工作线程(PoolWorker):线程池中的线程在没有任务的时候处于等待状态,可以循环的执行任务;
- 任务接口(Task):每个任务必须实现接口,用来提供工作线程调度任务的执行,规定了任务的入口以及执行结束的收尾工作和任务的执行状态等;
- 任务队列:用于存放没有处理的任务,提供一种缓存机制。
线程池的继承架构
Executor是一个顶层接口,在它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable类型,从字面意思可以理解,就是用来执行传进去的任务的;然后ExecutorService接口继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等;抽象类AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法;然后ThreadPoolExecutor继承了类AbstractExecutorService。
比较重要的类:
| ExecutorService | 真正的线程池接口。 |
| ScheduledExecutorService | 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。 |
| ThreadPoolExecutor | ExecutorService的默认实现。 |
| ScheduledThreadPoolExecutor | 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。 |
线程池状态
线程池的5种状态:Running、ShutDown、Stop、Tidying、Terminated。
| 状态 | 说明 | 状态切换 |
| RUNNING | 线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。创建时会调用此语句: private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); | 线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0! |
| SHUTDOWN | 线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。 | 调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。 |
| STOP | 线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。 | 调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。 |
| TIDYING | 当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。 | 当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。 当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。 |
| TERMINATED | 线程池彻底终止,就变成TERMINATED状态。 | 线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。 |
任务分类
根据任务所需要的cpu和io资源的量可以分为:
- CPU密集型任务:
- 主要是执行计算任务。响应时间很快,cpu一直在运行,这种任务cpu的利用率很高。
- 线程池大小太大对程序性能而言,反而是不利的,但最少也不应低于处理器的核心数。因为当有多个任务处于就绪状态时,处理器核心需要在线程间频繁进行上下文切换,而这种切换对程序性能损耗较大。
- IO密集型任务
- 主要是进行IO操作,执行IO操作的时间较长,这时cpu处于空闲状态,导致cpu的利用率不高。
- 当一个任务执行IO操作时,其线程将被阻塞,于是处理器可以立即进行上下文切换以便处理其他就绪线程。如果我们只有处理器可用核心数那么多个线程的话,即使有待执行的任务也无法处理,因为我们已经拿不出更多的线程供处理器调度了。
CPU密集型任务与IO密集型任务区分方法
如果任务被阻塞的时间少于执行时间,即这些任务是计算密集型的,则程序所需线程数将随之减少,但最少也不应低于处理器的核心数。
如果任务被阻塞的时间大于执行时间,即该任务是IO密集型的,我们就需要创建比处理器核心数大几倍数量的线程。例如,如果任务有50%的时间处于阻塞状态,则程序所需线程数为处理器可用核心数的两倍。
常用线程池大小设置
- CPU密集型:核心线程数 = CPU核数 + 1
- IO密集型:核心线程数 = CPU核数 * 2 + 1
CPU核数可以用此法获得:Runtime.getRuntime().availableProcessors()
对于计算密集型的任务,一个有N个处理器的系统通常使用一个N+1个线程的线程池来获得最优的利用率(计算密集型的线程恰好在某时因为发生一个页错误或者因其它原因而暂停,刚好有一个"额外"的线程,可以确保在这种情况下CPU周期不会中断工作)。
计算公式
N = CPU的数量
U = 期望的CPU的使用率,介于0-1之间
f:阻塞系数(阻塞时间占总时间的比例。总时间:阻塞时间 + 执行时间)
线程池大小 = N * U / (1 - f) //一个完全阻塞的任务是注定要挂掉的,无须担心阻塞系数会达到1。
举例:CPU核心数是4,期望cpu的使用率是100%,假设等待时间是4秒,计算时间是1秒。那么我最优的池大小就是:
4 * 100% / (1 - 4/5) = 20
线程池使用步骤步骤
1、创建一个线程池对象,控制要创建几个线程对象。
2、实现线程:
法1:新建一个类实现Runnable或者Callable接口。
法2:直接用lambda表达式传值。比如:
public class Demo {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.execute(Demo::myRun);
}
public static void myRun() {
System.out.println("hello world");
}
}
3、提交线程调用如下任一方法即可
void execute(Runnable command); Future> submit(Runnable task)Future submit(Callable task) Future submit(Runnable task, T result)
4、结束线程池。
submit 与 execute
| 项 | submit | execute |
| 说明 | 是ExecutorService中的方法。 用以提交一个任务。 | 是Executor接口的方法。 在未来某个时间执行给定命令。该命令可能在新的线程、已入池的线程或正调用的线程中执行,由 Executor 的实现决定。 |
| 方法原型 | //提交一个返回值的任务用于执行 //返回一个表示任务的未决结果的 Future。 //提交一个 Runnable 任务用于执行 //返回一个表示该任务的 Future。 //提交一个 Runnable 任务用于执行 //返回一个表示该任务的 Future。 | void execute(Runnable command) |
| 返回值 | 返回值是future对象 可以获取执行结果 | 没有返回值 |
Future
Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。计算完成后只能使用 get 方法来获取结果,如有必要,计算完成前可以阻塞此方法。取消则由 cancel 方法来执行。还提供了其他方法,以确定任务是正常完成还是被取消了。一旦计算完成,就不能再取消计算。
如果为了可取消性而使用 Future 但又不提供可用的结果,则可以声明 Future> 形式类型、并返回 null 作为底层任务的结果。 Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
也就是说Future提供了三种功能:
--判断任务是否完成;
--能够中断任务;
--能够获取任务执行结果。
- boolean cancel(boolean mayInterruptIfRunning) 试图取消对此任务的执行。
- V get() 如有必要,等待计算完成,然后获取其结果。
- V get(long timeout, TimeUnit unit) 如有必要,最多等待为使计算完成所给定的时间之后,获取其结果(如果结果可用)。
- boolean isCancelled() 如果在任务正常完成前将其取消,则返回 true。
- boolean isDone() 如果任务已完成,则返回 true。
关闭线程池
void shutdown() 启动一次顺序关闭,等待执行以前提交的任务完成,但不接受新任务。
List
流程图
- 提交任务
- 线程池判断核心线程池里的线程是否已经满了(全都在执行任务)。
- 如果不是:看线程数是否到达指定的核心线程池的大小
- 如果不是:则创建一个新的线程来执行任务。
- 如果是:使用空闲的线程来执行任务
- 如果是,则进入下个流程。
- 如果不是:看线程数是否到达指定的核心线程池的大小
- 线程池判断工作队列是否已经饱满。
- 如果没有满,则将新提交的任务存储在这工作队列里。
- 如果工作队列满了,则进入下一个流程。
- 队列见:多线程--BlockingQueue_feiying0canglang的博客-CSDN博客
- 线程池判断线程池的所有线程(最大线程数)是否已经满了(全都在执行任务)。
- 如果不是:看线程数是否到达指定的最大线程池的大小
- 如果不是:则创建一个新的线程来执行任务。
- 如果是:使用空闲的线程来执行任务
- 如果是:则交给饱和策略来处理这个任务。(饱和策略见下方)
- 如果不是:看线程数是否到达指定的最大线程池的大小
| 饱和策略 | 说明 |
| ThreadPoolExecutor.AbortPolicy | 为Java线程池默认的阻塞策略。 不执行此任务,而且直接抛出一个运行时异常(未检查的异常RejectedExecutionException)。切记ThreadPoolExecutor.execute需要try catch,否则程序会直接退出。 |
| ThreadPoolExecutor.DiscardPolicy | 直接抛弃,任务不执行,空方法。 |
| ThreadPoolExecutor.DiscardOldestPolicy | 从队列里删除最老的任务(头部的一个任务),并再次execute 此task。 |
| ThreadPoolExecutor.CallerRunsPolicy | 让调用execute方法的线程执行此command,会阻塞入口。 为调节机制,既不抛弃任务也不抛出异常,而是将某些任务回退到调用者,让调用者所在的线程去执行去执行。 |
| 用户自定义拒绝策略(最常用) | 实现RejectedExecutionHandler,并自己定义策略模式 |
例程
package concurrency.pool;
import java.util.concurrent.linkedBlockingDeque;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class SaturationPolicy {
public static void policy(RejectedExecutionHandler handler){
//基本线程2个,最大线程数为3,工作队列容量为5
ThreadPoolExecutor exec = new ThreadPoolExecutor(2,3,0, TimeUnit.MILLISECONDS,new linkedBlockingDeque<>(5));
if (handler != null){
exec.setRejectedExecutionHandler(handler);//设置饱和策略
}
for (int i = 0; i < 10; i++) {
exec.submit(new Task());//提交任务
}
exec.shutdown();
}
//自定义任务
static class Task implements Runnable {
private static int count = 0;
private int id = 0;//任务标识
public Task() {
id = ++count;
}
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(3);//休眠3秒
} catch (InterruptedException e) {
System.err.println("线程被中断" + e.getMessage());
}
System.out.println(" 任务:" + id + "t 工作线程: "+ Thread.currentThread().getName() + " 执行完毕");
}
}
public static void main(String[] args) {
// policy(new ThreadPoolExecutor.AbortPolicy());
// policy((new ThreadPoolExecutor.CallerRunsPolicy()));
// policy(new ThreadPoolExecutor.DiscardPolicy());
// policy(new ThreadPoolExecutor.DiscardOldestPolicy());
}
}
1.Abort策略:默认策略。
新任务提交时直接抛出未检查的异常RejectedExecutionException,该异常可由调用者捕获。
在主函数中添加如下代码:
policy(new ThreadPoolExecutor.AbortPolicy());
运行结果:
程序抛出了RejectedExecutionException,并且一共运行了8个任务(线程池开始能运行3个任务,工作队列中存储5个队列)。当工作队列满了的时候,直接抛出了异常,而且JVM一直不退出(我现在也不知道什么原因)。我们可以看到执行任务的线程全是线程池中的线程。
2.Discard策略。新提交的任务被抛弃。
在main函数中运行
policy(new ThreadPoolExecutor.DiscardPolicy());
运行结果:
通过上面的结果可以显示:没有异常抛出,后面提交的2个新任务被抛弃,只处理了前8(3+5)个任务,JVM退出。
3.DiscardOldest策略。队列的是“队头”的任务,然后尝试提交新的任务。(不适合工作队列为优先队列场景)
在main函数中运行如下方法
policy(new ThreadPoolExecutor.DiscardOldestPolicy());
运行结果:一共运行8个任务,程序结束,后面添加的任务9,任务10被执行了,而前面的任务3,任务4被丢弃。
4.CallerRuns策略
在主函数运行:
policy((new ThreadPoolExecutor.CallerRunsPolicy()));
运行结果
所有的任务都被运行,且有2(10 - 3 -5)个任务是在main线程中执行成功的,8个任务在线程池中的线程执行的。
ThreadPoolExecutor 简介ThreadPoolExecutor 结构图
- 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。
- 如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
- 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。
- 如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。
方法参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize:线程池的核心线程数:即便是线程池里没有任何任务,也会有corePoolSize个线程在候着等任务。
- maximumPoolSize:最大线程数。超过此数量,会触发拒绝策略。
- keepAliveTime:线程的存活时间。当线程池里的线程数大于corePoolSize时,如果等了keepAliveTime时长还没有任务可执行,则线程退出。
- unit:这个用来指定keepAliveTime的单位,比如秒:TimeUnit.SECONDS。
- workQueue:一个阻塞队列,提交的任务将会被放到这个队列里。
- threadFactory:线程工厂,用来创建线程,主要是为了给线程起名字,默认工厂的线程名字:pool-1-thread-3。
- handler:拒绝策略,当线程池里线程被耗尽,且队列也满了的时候会调用。
java doc中,并不提倡我们直接使用ThreadPoolExecutor,而是使用Executors类中提供的几个静态方法来创建线程池,例如:Executors.newCachedThreadPool(); Executors.newSingleThreadExecutor(); 等。但实际上,很多大公司会明确要求使用创建ThreadPoolExecutor对象的方法来使用线程池,因为这样能使人明确各个参数的具体含义。Executors类中提供的静态方法都是调用了创建ThreadPoolExecutor对象的方法,例如:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new linkedBlockingQueue());
}
示例1:睡眠任务
代码
package com.example;
import java.util.concurrent.linkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
class MyTask implements Runnable {
private int taskNum;
public MyTask(int num) {
this.taskNum = num;
}
@Override
public void run() {
System.out.println("正在执行task " + taskNum);
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("-----------------------task " + taskNum + "执行完毕");
}
}
public class Demo {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10,
200, TimeUnit.MILLISECONDS,
new linkedBlockingDeque<>(4));
for (int i = 0; i < 14; i++) {
MyTask myTask = new MyTask(i);
executor.execute(myTask);
System.out.println("线程池中线程数目:" + executor.getPoolSize() + ",队列中等待执行的任务数目:" +
executor.getQueue().size() + ",已执行完别的任务数目:" + executor.getCompletedTaskCount());
}
executor.shutdown();
}
}
运行结果
Connected to the target VM, address: '127.0.0.1:58452', transport: 'socket' 正在执行task 0 线程池中线程数目:1,队列中等待执行的任务数目:0,已执行完别的任务数目:0 线程池中线程数目:2,队列中等待执行的任务数目:0,已执行完别的任务数目:0 线程池中线程数目:3,队列中等待执行的任务数目:0,已执行完别的任务数目:0 正在执行task 1 线程池中线程数目:4,队列中等待执行的任务数目:0,已执行完别的任务数目:0 正在执行task 2 线程池中线程数目:5,队列中等待执行的任务数目:0,已执行完别的任务数目:0 正在执行task 3 正在执行task 4 线程池中线程数目:5,队列中等待执行的任务数目:1,已执行完别的任务数目:0 线程池中线程数目:5,队列中等待执行的任务数目:2,已执行完别的任务数目:0 线程池中线程数目:5,队列中等待执行的任务数目:3,已执行完别的任务数目:0 线程池中线程数目:5,队列中等待执行的任务数目:4,已执行完别的任务数目:0 线程池中线程数目:6,队列中等待执行的任务数目:4,已执行完别的任务数目:0 线程池中线程数目:7,队列中等待执行的任务数目:4,已执行完别的任务数目:0 正在执行task 9 正在执行task 10 线程池中线程数目:8,队列中等待执行的任务数目:4,已执行完别的任务数目:0 线程池中线程数目:9,队列中等待执行的任务数目:4,已执行完别的任务数目:0 正在执行task 11 正在执行task 12 线程池中线程数目:10,队列中等待执行的任务数目:4,已执行完别的任务数目:0 正在执行task 13 -----------------------task 3执行完毕 -----------------------task 2执行完毕 -----------------------task 1执行完毕 正在执行task 5 -----------------------task 4执行完毕 正在执行task 7 正在执行task 8 -----------------------task 0执行完毕 -----------------------task 11执行完毕 -----------------------task 10执行完毕 -----------------------task 9执行完毕 正在执行task 6 -----------------------task 13执行完毕 -----------------------task 12执行完毕 -----------------------task 5执行完毕 -----------------------task 8执行完毕 -----------------------task 6执行完毕 -----------------------task 7执行完毕 Disconnected from the target VM, address: '127.0.0.1:58452', transport: 'socket' Process finished with exit code 0
从执行结果可以看出:
- 当线程池中线程的数目大于5时,便将任务放入任务缓存队列里面,当任务缓存队列满了之后,便创建新的线程。
- 线程都执行完毕后,程序可以正常退出。
如果上面程序中,将for循环改成15,就会抛出任务拒绝异常了,日志如下:
Connected to the target VM, address: '127.0.0.1:58469', transport: 'socket' 正在执行task 0 线程池中线程数目:1,队列中等待执行的任务数目:0,已执行完别的任务数目:0 线程池中线程数目:2,队列中等待执行的任务数目:0,已执行完别的任务数目:0 线程池中线程数目:3,队列中等待执行的任务数目:0,已执行完别的任务数目:0 正在执行task 1 正在执行task 2 线程池中线程数目:4,队列中等待执行的任务数目:0,已执行完别的任务数目:0 正在执行task 3 线程池中线程数目:5,队列中等待执行的任务数目:0,已执行完别的任务数目:0 正在执行task 4 线程池中线程数目:5,队列中等待执行的任务数目:1,已执行完别的任务数目:0 线程池中线程数目:5,队列中等待执行的任务数目:2,已执行完别的任务数目:0 线程池中线程数目:5,队列中等待执行的任务数目:3,已执行完别的任务数目:0 线程池中线程数目:5,队列中等待执行的任务数目:4,已执行完别的任务数目:0 线程池中线程数目:6,队列中等待执行的任务数目:4,已执行完别的任务数目:0 线程池中线程数目:7,队列中等待执行的任务数目:4,已执行完别的任务数目:0 正在执行task 9 线程池中线程数目:8,队列中等待执行的任务数目:4,已执行完别的任务数目:0 正在执行task 10 线程池中线程数目:9,队列中等待执行的任务数目:4,已执行完别的任务数目:0 正在执行task 11 线程池中线程数目:10,队列中等待执行的任务数目:4,已执行完别的任务数目:0 正在执行task 12 正在执行task 13 Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.example.MyTask@27ddd392 rejected from java.util.concurrent.ThreadPoolExecutor@19e1023e[Running, pool size = 10, active threads = 10, queued tasks = 4, completed tasks = 0] at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063) at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830) at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379) at com.example.Demo.main(Demo.java:34) -----------------------task 4执行完毕 -----------------------task 1执行完毕 -----------------------task 9执行完毕 -----------------------task 0执行完毕 -----------------------task 13执行完毕 -----------------------task 3执行完毕 -----------------------task 2执行完毕 -----------------------task 12执行完毕 -----------------------task 11执行完毕 -----------------------task 10执行完毕 正在执行task 8 正在执行task 6 正在执行task 5 正在执行task 7 -----------------------task 7执行完毕 -----------------------task 6执行完毕 -----------------------task 5执行完毕 -----------------------task 8执行完毕
结果:
- 在执行第15个线程时,直接抛出了异常,但没有影响其他进程的运行
- 程序无法正常退出
本处只是修改了上边的睡眠任务为死循环,其他不变
package com.example;
import java.util.concurrent.linkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
class MyTask implements Runnable {
private int taskNum;
public MyTask(int num) {
this.taskNum = num;
}
@Override
public void run() {
System.out.println("正在执行task " + taskNum);
while (true) ;
}
}
public class Demo {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10,
200, TimeUnit.MILLISECONDS,
new linkedBlockingQueue<>(4));
for (int i = 0; i < 14; i++) {
MyTask myTask = new MyTask(i);
executor.execute(myTask);
System.out.println("线程池中线程数目:" + executor.getPoolSize() + ",队列中等待执行的任务数目:" +
executor.getQueue().size() + ",已执行完别的任务数目:" + executor.getCompletedTaskCount());
}
executor.shutdown();
}
}
执行结果:(程序无法退出)
Connected to the target VM, address: '127.0.0.1:60128', transport: 'socket' 正在执行task 0 线程池中线程数目:1,队列中等待执行的任务数目:0,已执行完别的任务数目:0 线程池中线程数目:2,队列中等待执行的任务数目:0,已执行完别的任务数目:0 线程池中线程数目:3,队列中等待执行的任务数目:0,已执行完别的任务数目:0 正在执行task 1 线程池中线程数目:4,队列中等待执行的任务数目:0,已执行完别的任务数目:0 线程池中线程数目:5,队列中等待执行的任务数目:0,已执行完别的任务数目:0 线程池中线程数目:5,队列中等待执行的任务数目:1,已执行完别的任务数目:0 线程池中线程数目:5,队列中等待执行的任务数目:2,已执行完别的任务数目:0 线程池中线程数目:5,队列中等待执行的任务数目:3,已执行完别的任务数目:0 正在执行task 3 线程池中线程数目:5,队列中等待执行的任务数目:4,已执行完别的任务数目:0 正在执行task 4 线程池中线程数目:6,队列中等待执行的任务数目:4,已执行完别的任务数目:0 正在执行task 2 线程池中线程数目:7,队列中等待执行的任务数目:4,已执行完别的任务数目:0 线程池中线程数目:8,队列中等待执行的任务数目:4,已执行完别的任务数目:0 线程池中线程数目:9,队列中等待执行的任务数目:4,已执行完别的任务数目:0 线程池中线程数目:10,队列中等待执行的任务数目:4,已执行完别的任务数目:0 正在执行task 9 正在执行task 10 正在执行task 12 正在执行task 11 正在执行task 13异常处理 简介
简介
当抛出RuntimeException异常时,线程就会终结,而对于主线程和其他线程完全不受影响,且完全感知不到某个线程抛出的异常(也是说完全无法catch到这个异常)。
例解
package org.example.a;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo {
public static void main(String[] args) {
ExecutorService executorService= Executors.newFixedThreadPool(3);
executorService.execute(()->{
int i = 1/0;
});
executorService.execute(()->{
System.out.println("Normal task");
});
}
}
运行结果
Exception in thread "pool-1-thread-1" java.lang.ArithmeticException: / by zero at org.example.a.Demo.lambda$main$0(Demo.java:11) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) Normal task捕获线程池异常的方案
如何才能获取到线程池里面的任务抛出的异常?
方案1:整个任务try-catch起来,捕获里面的异常(每个任务都加一个try-catch太麻烦了,而且代码也不好看)
package org.example.a;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
executorService.execute(() -> {
try {
int i = 1 / 0;
}catch (Exception e){
System.out.println(e.getMessage());
}
});
executorService.execute(() -> {
System.out.println("Normal task");
});
}
}
运行结果
/ by zero Normal task将execute改为submit之后的运行结果
Normal task
方案2:设置线程的UncaughtExceptionHandler
package org.example.a; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Demo { public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(1); Thread thread=new Thread(() -> { int i = 1 / 0; }); thread.setDefaultUncaughtExceptionHandler((Thread t, Throwable e) -> { System.out.println("exceptionHandler: " + e.getMessage()); }); executorService.execute(thread); } }运行结果
exceptionHandler: / by zero
方案3:指定线程池工厂方法,设置其线程的UncaughtExceptionHandler
原理package org.example.a; import java.util.concurrent.*; public class Demo { public static void main(String[] args) { //1.实现一个自己的线程池工厂 ThreadFactory factory = (Runnable r) -> { //创建一个线程 Thread t = new Thread(r); //给创建的线程设置UncaughtExceptionHandler对象 里面实现异常的默认逻辑 t.setDefaultUncaughtExceptionHandler((Thread thread1, Throwable e) -> { System.out.println("ThreadFactory: exceptionHandler" + e.getMessage()); }); return t; }; //2.创建一个自己定义的线程池,使用自己定义的线程工厂 ExecutorService service = new ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS,new linkedBlockingQueue(10), factory); //3.提交任务 service.execute(()->{ int i=1/0; }); } }执行结果
ThreadFactory: exceptionHandler/ by zero注意:将execute改为submit则无任何输出。
其他网址
线程池异常处理详解,一文搞懂_helloworld-CSDN博客_线程池异常
execute和submit有什么区别?
execute
execute提交的任务,会被封装成一个Runable任务,然后Runable对象被封装成一个Worker,最后在Worker的run方法里面跑runWoker方法, 里面再又调了我们最初的参数 Runable任务的任务,并且用try-catch捕获了异常,会被直接抛出去,因此我们在execute中看到了我们的任务的异常信息。
submit
submit提交的任务,会被封装成一个Runable任务,然后Runable对象被封装成一个FutureTask ,里面的 run 方法 try-catch了所有的异常,并设置到了outcome(Object类型)里面, 可以通过FutureTask .get获取到outcome。
所以在submit提交的时候,里面发生了异常, 是不会有任何抛出信息的。在submit里面,除了从返回结果里面取到异常之外, 没有其他方法了。
因此,在不需要返回结果的情况下,最好用execute ,这样如果疏漏了异常捕获,也不至于丢掉异常信息。



