Skip to content

Latest commit

 

History

History
118 lines (97 loc) · 7.23 KB

异常处理机制.md

File metadata and controls

118 lines (97 loc) · 7.23 KB

本篇是在写另外一篇线程池的时候涉及到java异常处理机制,发现知识点还蛮多的,可以单独成文,所以拎出来写一篇。
不信你先看下如下问题,如果在面试过程被问到,能否回答。

  • java线程执行异常处理机制是怎么样的?UncaughtExceptionHandler接口有什么作用?
  • 提交一个任务到线程池,执行异常,未捕获处理,这个线程会怎么样?
  • 如下程序会输出“execute error”吗?会输出“submit error”吗?
	public void test() throws InterruptedException {
		ExecutorService executorService = Executors.newFixedThreadPool(1);
		executorService.execute(() -> {
			//业务处理
			throw new RuntimeException("execute error");
		});
		executorService.submit(() -> {
			//业务处理
			throw new RuntimeException("submit error");
		});
		Thread.sleep(10000);		
	}

正文

线程是cpu的最小执行单元,java中的Thread就是对应一个内核线程,也就是说当我们new Thread()并调用start方法时,系统会创建一个线程,并提交给cpu,等待分配时间片处理。jdk21开始引入虚拟线程,类似于golang里的协程,与内核线程就不再是1:1的关系,而是n:1。通过在用户层面进行任务调度,可以减少系统线程的创建和切换,提升性能,虚拟线程这里我们不做讨论。
java代码首先要经过编译器编译为字节码,再交由jvm执行,当在执行过程jvm发现程序异常时,就会将异常抛出,由线程异常处理器处理。具体是调用Thread的dispatchUncaughtException方法,并把异常作为参数传递,这是个私有方法,由jvm负责调用,如下:

image

UncaughtExceptionHandler就是定义在Thread的内部类,表示未捕获异常处理器,我们可以在创建线程的时候给它赋值,例如:

	public void test() throws InterruptedException {
		Thread thread = new Thread(() -> {
			throw new RuntimeException("error");
		});
		thread.setUncaughtExceptionHandler((t, e) -> {
			e.printStackTrace(System.err);
		});

		thread.start();
		Thread.sleep(3000);
	}

那么平时我们没有定义异常处理器,它又是怎么处理的呢?如下可以看到当没有设置时,使用的是ThreadGroup对象,ThreadGroup也实现了UncaughtExceptionHandler接口,它的uncaughtException方法如下。

image

image

可以看到它会判断是否有父ThreadGroup,如果有就往上抛,最终肯定会抛到一个顶层的ThreadGroup(name为system),也就是执行图中的else部分。
首先会通过Thread.getDefaultUncaughtExceptionHandler()获取全局默认的异常处理器,这个处理器可以通过Thread.setDefaultUncaughtExceptionHandler()静态方法进行设置,对所有线程生效。如果有则使用全局默认处理器进行处理,否则将线程名称和异常堆栈打印到标准错误输出System.err,也就是控制台。

image

备注:图中ThreadDeath异常是在调用Thread stop方法时抛出的异常,这是个废弃方法,我们忽略它即可。
画张图总结一下整个流程,如下:

image

线程池

这里以TheradPoolExecutor为例,我们知道提交任务给线程池可以用execute和submit方法,前者参数是Runnable无法获得返回值,后缀参数是Callable可以获得返回值。如开头问题,两者在异常处理方面也有一些差别。

	public void test() throws InterruptedException {
		ExecutorService executorService = Executors.newFixedThreadPool(1);
		executorService.execute(() -> {
			//业务处理
			throw new RuntimeException("execute error");
		});
		executorService.submit(() -> {
			//业务处理
			throw new RuntimeException("submit error");
		});
		Thread.sleep(10000);		
	}

问题答案是会打印“execute error”的异常堆栈,“submit error”的则不会打印。
我们先看execute方法,在线程池内部,Thread对象会被包装为Worker对象,最终会执行ThreadPoolExecutor的runWorker方法,它会对异常进行捕获,并重新抛出,接着就又回到我们前面分析的Thread异常处理机制,最终打印到控制台。

image

另外从图中还可以看到,如果执行异常会抛出中断循环,最终由processWorkerExit方法处理,该方法会把worker移除(线程对象),也就是本次使用的线程将被抛弃。

image

如下代码,一个线程数为1的线程池,可以看到输出两个不同的线程id,因为第一个线程执行任务异常,被丢弃了。

	@Test    
	public void test2() throws InterruptedException {     
		ExecutorService executorService = Executors.newFixedThreadPool(1);    
		executorService.execute(() -> {    
			//业务处理    
			System.out.println(Thread.currentThread().getId());   
			throw new RuntimeException("error");   
		});
		executorService.execute(() -> {
			//业务处理
			System.out.println(Thread.currentThread().getId());
		});
		Thread.sleep(10000);		
	}

submit方法则不同,它是通过FutureTask来执行任务,从源码可以看到最终是执行FutureTask的run方法,它捕获了异常但没有抛出,而是保存在outcome变量中。outcome就是Callable参数对应的返回值,如果有异常返回的就是异常对象,如果没有异常就是正常返回值。
如果我们想submit也能抛出异常,需要调用它的get()方法,它会判断如果有异常,就抛出。

image

image

image

使用new Thread()创建线程时,可以调用setUncaughtExceptionHandler设置UncaughtExceptionHandler,但上面我们通过Executors.newFixedThreadPool,或者new ThreadPoolExecutor创建线程池时,就没有参数可以直接设置UncaughtExceptionHandler了,可以通过实现ThreadFactory接口来指定,我们使用guava可以这样创建:

	new ThreadFactoryBuilder()
		.setNameFormat(poolName + "-%d")
		.setUncaughtExceptionHandler((t, e) -> {
			//handle exception
	}).build();

注意,生产环境我们需要将异常堆栈输出到日志文件,所以尽管execute方法会抛出异常,但如果你没有对其进行捕获处理,也没有设置异常处理器,最终也还是丢失,我们就出现过因为线程池内任务没捕获异常没打印异常日志,排查问题非常麻烦。好了,本篇我们得出一个结论:开启异步线程要进行异常捕获处理。