Java 프로그램을 시작하는 것은 본질적으로 Java 클래스의 기본 메소드를 실행하는 것입니다. 무한 루프 프로그램을 작성하고 실행한 다음 jvisualvm
을 실행하여 관찰해 보겠습니다. jvisualvm
进行观察
可以看到这个Java进程中,一共有11个线程,其中10个守护线程,1个用户线程。我们main方法中的代码,就跑在一个名为main
的线程中。当Java进程中跑着的所有线程都是守护线程时,JVM就会退出。
在单线程的场景下,如果代码运行到某个位置时抛出了异常,会看到控制台打印出异常的堆栈信息。但在多线程的场景下,子线程中发生的异常,不一定就能及时的将异常信息打印出来。
我曾经在工作中遇到过一次,采用CompletableFuture.runAsync
异步处理耗时任务时,任务处理过程中出现异常,然而日志中没有任何关于异常的信息。时隔许久,重新温习了线程中的异常处理机制,加深了对线程工作原理的理解,特此记录。
我们知道,Java程序的运行,是先经由javac
将Java源代码编译成class字节码文件,然后由JVM加载并解析class文件,随后从主类的main方法开始执行。当一个线程在运行过程中抛出了未捕获异常时,会由JVM调用这个线程对象上的dispatchUncaughtException
方法,进行异常处理。
// Thread类中 private void dispatchUncaughtException(Throwable e) { getUncaughtExceptionHandler().uncaughtException(this, e); }
源码很好理解,先获取一个UncaughtExceptionHandler
异常处理器,然后通过调用这个异常处理器的uncaughtException
方法来对异常进行处理。(下文用缩写ueh
来表示UncaughtExceptionHandler
)
ueh
是个 啥呢?其实就是定义在Thread
内部的一个接口,用作异常处理。
@FunctionalInterface public interface UncaughtExceptionHandler { /** * Method invoked when the given thread terminates due to the * given uncaught exception. * <p>Any exception thrown by this method will be ignored by the * Java Virtual Machine. * @param t the thread * @param e the exception */ void uncaughtException(Thread t, Throwable e); }
再来看下Thread
对象中的getUncaughtExceptionHandler
方法
public UncaughtExceptionHandler getUncaughtExceptionHandler() { return uncaughtExceptionHandler != null ? uncaughtExceptionHandler : group; }
先查看当前这个Thread
对象是否有设置自定义的ueh
对象,若有,则由其对异常进行处理,否则,由当前Thread
对象所属的线程组(ThreadGroup
)进行异常处理。我们点开源码,容易发现ThreadGroup
类本身实现了Thread.UncaughtExceptionHandler
接口,也就是说ThreadGroup
本身就是个异常处理器。
public class ThreadGroup implements Thread.UncaughtExceptionHandler { private final ThreadGroup parent; .... }
假设我们在main
方法中抛出一个异常,若没有对main
线程设置自定义的ueh
对象,则交由main
线程所属的ThreadGroup
来处理异常。我们看下ThreadGroup
是怎么处理异常的:
public void uncaughtException(Thread t, Throwable e) { if (parent != null) { parent.uncaughtException(t, e); } else { Thread.UncaughtExceptionHandler ueh = Thread.getDefaultUncaughtExceptionHandler(); if (ueh != null) { ueh.uncaughtException(t, e); } else if (!(e instanceof ThreadDeath)) { System.err.print("Exception in thread \"" + t.getName() + "\" "); e.printStackTrace(System.err); } } }
这部分源码也比较简短。首先是查看当前ThreadGroup
是否拥有父级的ThreadGroup
,若有,则调用父级ThreadGroup
进行异常处理。否则,调用静态方法Thread.getDefaultUncaughtExceptionHandler()
获取一个默认的ueh
对象。
若默认的ueh
对象不为空,则由这个默认的ueh
对象进行异常处理;否则,当异常不是ThreadDeath
时,直接将当前线程的名字,和异常的堆栈信息,通过标准错误输出(System.err
)打印到控制台。
我们随便运行一个main
方法,看一下线程的情况
可以看到,main
线程属于一个同样名为main
的ThreadGroup
,而这个main
的ThreadGroup
,其父级ThreadGroup
名为system
,而这个system
的ThreadGroup
,没有父级了,它就是根ThreadGroup
。
由此可知,main
线程中抛出的未捕获异常,最终会交由名为system
的ThreadGroup
进行异常处理,而由于没有设置默认的ueh
对象,异常信息会通过System.err
输出到控制台。
接下来,我们通过最朴素的方式(new
一个Thread
),在main
main
이라는 스레드에서 실행됩니다. Java 프로세스에서 실행 중인 모든 스레드가 데몬 스레드인 경우 JVM이 종료됩니다. 🎜🎜단일 스레드 시나리오에서 코드가 특정 위치로 실행될 때 예외가 발생하면 콘솔에 인쇄된 예외 스택 정보를 볼 수 있습니다. 그러나 다중 스레드 시나리오에서는 하위 스레드에서 발생하는 예외가 반드시 적시에 예외 정보를 인쇄하지 않을 수도 있습니다. 🎜🎜직장에서 CompletableFuture.runAsync
를 사용하여 시간이 많이 걸리는 작업을 비동기적으로 처리할 때 작업 처리 중에 예외가 발생했지만 로그에 예외에 대한 정보가 없는 것을 본 적이 있습니다. 오랜만에 스레드의 예외 처리 메커니즘을 다시 살펴보고 스레드의 작동 원리에 대한 이해를 심화시켜 기록합니다. 🎜🎜스레드의 예외 처리 메커니즘🎜🎜우리는 Java 프로그램을 실행하려면 먼저 javac
를 통해 Java 소스 코드를 클래스 바이트코드 파일로 컴파일한 다음 JVM이 클래스 파일을 로드하고 구문 분석한다는 것을 알고 있습니다. , 그러면 메인 클래스의 메인 메소드에서 실행이 시작됩니다. 스레드가 작업 중에 잡히지 않는 예외를 발생시키면 JVM은 예외 처리를 위해 스레드 객체에서 dispatchUncaughtException
메서드를 호출합니다. 🎜public static void main(String[] args) { Thread thread = new Thread(() -> { System.out.println(3 / 0); }); thread.start(); }
UncaughtExceptionHandler
예외 핸들러를 얻은 다음 이 예외 핸들러의 uncaughtException
메서드를 호출하여 예외를 처리하세요. (아래에서 약어 ueh
는 UncaughtExceptionHandler
를 나타냅니다.) 🎜🎜ueh
가 무엇인가요? 실제로는 Thread
내부에 정의된 인터페이스로 예외 처리에 사용됩니다. 🎜public static void main(String[] args) { ExecutorService threadPool = Executors.newSingleThreadExecutor(); threadPool.execute(() -> { System.out.println(3 / 0); }); }
Thread
개체의 getUncaughtExceptionHandler
메서드를 살펴보겠습니다. 🎜public static void main(String[] args) { ExecutorService threadPool = Executors.newSingleThreadExecutor(); threadPool.submit(() -> { System.out.println(3 / 0); }); }
Thread
개체에 사용자 정의 가 있는지 확인하세요. code> >ueh
개체가 있는 경우 예외를 처리합니다. 그렇지 않으면 현재 Thread
개체가 속한 스레드 그룹(ThreadGroup
)이 예외를 처리합니다. 예외를 처리합니다. 오픈 소스 코드를 클릭하면 ThreadGroup
클래스 자체가 Thread.UncaughtExceptionHandler
인터페이스를 구현한다는 것을 쉽게 찾을 수 있습니다. 이는 ThreadGroup
그 자체는 예외 처리기입니다. 🎜Worker(Runnable firstTask) { setState(-1); // inhibit interrupts until runWorker this.firstTask = firstTask; this.thread = getThreadFactory().newThread(this); }
main
메서드에서 예외가 발생한다고 가정해 보겠습니다. 사용자 정의 ueh
개체가 main
스레드에 대해 설정되지 않은 경우 main
스레드가 속한 ThreadGroup
을 사용하여 예외를 처리합니다. ThreadGroup
이 예외를 처리하는 방법을 살펴보겠습니다. 🎜public Future<?> submit(Runnable task) { if (task == null) throw new NullPointerException(); RunnableFuture<Void> ftask = newTaskFor(task, null); execute(ftask); return ftask; }
ThreadGroup
에 상위 ThreadGroup
이 있는지 확인하는 것입니다. 그렇다면 예외 처리를 위해 상위 ThreadGroup
를 호출하세요. 그렇지 않은 경우 정적 메서드 Thread.getDefaultUncaughtExceptionHandler()
를 호출하여 기본 ueh
개체를 가져옵니다. 🎜🎜기본 ueh
개체가 비어 있지 않으면 기본 ueh
개체가 예외 처리를 수행합니다. 그렇지 않으면 예외가 ThreadDeath
, 표준 오류 출력(System.err
)을 통해 현재 스레드 이름과 예외 스택 정보를 직접 출력합니다. ) 콘솔에 인쇄됩니다. 🎜🎜main
메소드를 실행하고 스레드 상황을 살펴보겠습니다🎜🎜🎜🎜🎜🎜main
스레드가 main<이라는 <code>ThreadGroup
에 속해 있음을 알 수 있습니다. /code>, 이 main
의 ThreadGroup
에는 system
이라는 상위 ThreadGroup
이 있고 이 시스템
의 ThreadGroup
에는 상위가 없으며 루트 ThreadGroup
입니다. 🎜🎜main
스레드에서 발생한 포착되지 않은 예외는 결국 예외 처리를 위해 system
이라는 ThreadGroup
으로 넘겨지는 것을 볼 수 있습니다. default의 ueh
객체가 설정되지 않았으므로 예외 정보는 System.err
를 통해 콘솔에 출력됩니다. 🎜🎜다음으로 가장 간단한 방법으로 메인
스레드에 하위 스레드를 생성하고(새
스레드
), 스레드 예외를 발생시킬 수 있는 코드를 작성하고 관찰하세요🎜public static void main(String[] args) { Thread thread = new Thread(() -> { System.out.println(3 / 0); }); thread.start(); }
子线程中的异常信息被打印到了控制台。异常处理的流程就是我们上面描述的那样。
所以,正常来说,如果没有对某个线程设置特定的ueh
对象;也没有调用静态方法Thread.setDefaultUncaughtExceptionHandler
设置全局默认的ueh
对象。那么,在任意一个线程的运行过程中抛出未捕获异常时,异常信息都会被输出到控制台(当异常是ThreadDeath
时则不会进行输出,但通常来说,异常都不是ThreadDeath
,不过这个细节要注意下)。
如何设置自定义的ueh
对象来进行异常处理?根据上面的分析可知,有2种方式
对某一个Thread
对象,调用其setUncaughtExceptionHandler
方法,设置一个ueh
对象。注意这个ueh
对象只对这个线程起作用
调用静态方法Thread.setDefaultUncaughtExceptionHandler()
设置一个全局默认的ueh
对象。这样设置的ueh
对象会对所有线程起作用
当然,由于ThreadGroup
本身可以充当ueh
,所以其实还可以实现一个ThreadGroup
子类,重写其uncaughtException
方法进行异常处理。
若一个线程没有进行任何设置,当在这个线程内抛出异常后,默认会将线程名称和异常堆栈,通过System.err
进行输出。
在实际的开发中,我们经常会使用线程池来进行多线程的管理和控制,而不是通过new
来手动创建Thread
对象。
对于Java中的线程池ThreadPoolExecutor
,我们知道,通常来说有两种方式,可以向线程池提交任务:
execute
submit
其中execute
方法没有返回值,我们通过execute
提交的任务,只需要提交该任务给线程池执行,而不需要获取任务的执行结果。而submit
方法,会返回一个Future
对象,我们通过submit
提交的任务,可以通过这个Future
对象,拿到任务的执行结果。
我们分别尝试如下代码:
public static void main(String[] args) { ExecutorService threadPool = Executors.newSingleThreadExecutor(); threadPool.execute(() -> { System.out.println(3 / 0); }); }
public static void main(String[] args) { ExecutorService threadPool = Executors.newSingleThreadExecutor(); threadPool.submit(() -> { System.out.println(3 / 0); }); }
容易得到如下结果:
通过execute
方法提交的任务,异常信息被打印到控制台;通过submit
方法提交的任务,没有出现异常信息。
我们稍微跟一下ThreadPoolExecutor
的源码,当使用execute
方法提交任务时,在runWorker
方法中,会执行到下图红框的部分
在上面的代码执行完毕后,由于异常被throw
了出来,所以会由JVM捕捉到,并调用当前子线程的dispatchUncaughtException
方法进行处理,根据上面的分析,最终异常堆栈会被打印到控制台。
多扯几句别的。
上面跟源码时,注意到Worker
是ThreadPoolExecutor
的一个内部类,也就是说,每个Worker
都会隐式的持有ThreadPoolExecutor
对象的引用(内部类的相关原理请自行补课)。每个Worker
在运行时(在不同的子线程中运行)都能够对ThreadPoolExecutor
对象(通常来说这个对象是在main
线程中被维护)中的属性进行访问和修改。Worker
实现了Runnable
接口,并且其run
方法实际是调用的ThreadPoolExecutor
上的runWorker
方法。在新建一个Worker
时,会创建一个新的Thread
对象,并把当前Worker
的引用传递给这个Thread
对象,随后调用这个Thread
对象的start
方法,则开始在这个Thread
中(子线程中)运行这个Worker
。
Worker(Runnable firstTask) { setState(-1); // inhibit interrupts until runWorker this.firstTask = firstTask; this.thread = getThreadFactory().newThread(this); }
ThreadPoolExecutor
中的addWorker
方法
再次跟源码时,加深了对ThreadPoolExecutor
和Worker
体系的理解和认识。
它们之间有一种嵌套依赖的关系。每个Worker
里持有一个Thread
对象,这个Thread
对象又是以这个Worker
对象作为Runnable
,而Worker
又是ThreadPoolExecutor
的内部类,这意味着每个Worker
对象都会隐式的持有其所属的ThreadPoolExecutor
对象的引用。每个Worker
的run
方法, 都跑在子线程中,但是这些Worker
跑在子线程中时,能够对ThreadPoolExecutor
对象的属性进行访问和修改(每个Worker
的run
方法都是调用的runWorker
,所以runWorker
方法是跑在子线程中的,这个方法中会对线程池的状态进行访问和修改,比如当前子线程运行过程中抛出异常时,会从ThreadPoolExecutor
中移除当前Worker
,并启一个新的Worker
)。而通常来说,ThreadPoolExecutor
对象的引用,我们通常是在主线程中进行维护的。
反正就是这中间其实有点骚东西,没那么简单。需要多跟几次源码,多自己打断点进行debug,debug过程中可以通过IDEA的Evaluate Expression
功能实时观察当前方法执行时所处的线程环境(Thread.currentThread
)。
扯得有点远了,现在回到正题。上面说了调用ThreadPoolExecutor
中的execute
方法提交任务,子线程中出现异常时,异常会被抛出,打印在控制台,并且当前Worker
会被线程池回收,并重启一个新的Worker
作为替代。那么,调用submit
时,异常为何就没有被打印到控制台呢?
我们看一下源码:
public Future<?> submit(Runnable task) { if (task == null) throw new NullPointerException(); RunnableFuture<Void> ftask = newTaskFor(task, null); execute(ftask); return ftask; }
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) { return new FutureTask<T>(runnable, value); }
通过调用submit
提交的任务,被包装了成了一个FutureTask
对象,随后会将这个FutureTask
对象,通过execute
方法提交给线程池,并返回FutureTask
对象给主线程的调用者。
也就是说,submit
方法实际做了这几件事
将提交的Runnable
,包装成FutureTask
调用execute
方法提交这个FutureTask
(实际还是通过execute
提交的任务)
将FutureTask
作为返回值,返回给主线程的调用者
关键就在于FutureTask
,我们来看一下
public FutureTask(Runnable runnable, V result) { this.callable = Executors.callable(runnable, result); this.state = NEW; // ensure visibility of callable }
// Executors中 public static <T> Callable<T> callable(Runnable task, T result) { if (task == null) throw new NullPointerException(); return new RunnableAdapter<T>(task, result); }
static final class RunnableAdapter<T> implements Callable<T> { final Runnable task; final T result; RunnableAdapter(Runnable task, T result) { this.task = task; this.result = result; } public T call() { task.run(); return result; } }
通过submit
方法传入的Runnable
,通过一个适配器RunnableAdapter
转化为了Callable
对象,并最终包装成为一个FutureTask
对象。这个FutureTask
,又实现了Runnable
和Future
接口
于是我们看下FutureTask
的run
方法(因为最终是将包装后的FutureTask
提交给线程池执行,所以最终会执行FutureTask
的run
方法)
protected void setException(Throwable t) { if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { outcome = t; UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state finishCompletion(); } }
可以看到,异常信息只是被简单的设置到了FutureTask
的outcome
字段上。并没有往外抛,所以这里其实相当于把异常给生吞了,catch
块中捕捉到异常后,既没有打印异常的堆栈,也没有把异常继续往外throw
。所以我们无法在控制台看到异常信息,在实际的项目中,此种场景下的异常信息也不会被输出到日志文件。这一点要特别注意,会加大问题的排查难度。
那么,为什么要这样处理呢?
因为我们通过submit
提交任务时,会拿到一个Future
对象
public Future<?> submit(Runnable task) { if (task == null) throw new NullPointerException(); RunnableFuture<Void> ftask = newTaskFor(task, null); execute(ftask); return ftask; }
我们可以在稍后,通过Future
对象,来获知任务的执行情况,包括任务是否成功执行完毕,任务执行后返回的结果是什么,执行过程中是否出现异常。
所以,通过submit
提交的任务,实际会把任务的各种状态信息,都封装在FutureTask
对象中。当最后调用FutureTask
对象上的get
方法,尝试获取任务执行结果时,才能够看到异常信息被打印出来。
public V get() throws InterruptedException, ExecutionException { int s = state; if (s <= COMPLETING) s = awaitDone(false, 0L); return report(s); }
private V report(int s) throws ExecutionException { Object x = outcome; if (s == NORMAL) return (V)x; if (s >= CANCELLED) throw new CancellationException(); throw new ExecutionException((Throwable)x); // 异常会通过这一句被抛出来 }
通过ThreadPoolExecutor
的execute
方法提交的任务,出现异常后,异常会在子线程中被抛出,并被JVM捕获,并调用子线程的dispatchUncaughtException
方法,进行异常处理,若子线程没有任何特殊设置,则异常堆栈会被输出到System.err
,即异常会被打印到控制台上。并且会从线程池中移除当前Worker
,并另启一个新的Worker
作为替代。
通过ThreadPoolExecutor
的submit
方法提交的任务,任务会先被包装成FutureTask
对象,出现异常后,异常会被生吞,并暂存到FutureTask
对象中,作为任务执行结果的一部分。异常信息不会被打印,该子线程也不会被线程池移除(因为异常在子线程中被吞了,没有抛出来)。在调用FutureTask
上的get
方法时(此时一般是在主线程中了),异常才会被抛出,触发主线程的异常处理,并输出到System.err
其他的线程池场景
比如:
使用ScheduledThreadPoolExecutor
实现延迟任务或者定时任务(周期任务),分析过程也是类似。这里给个简单结论,当调用scheduleAtFixedRate
方法执行一个周期任务时(任务会被包装成FutureTask
(实际是ScheduledFutureTask
,是FutureTask
的子类)),若周期任务中出现异常,异常会被生吞,异常信息不会被打印,线程不会被回收,但是周期任务执行这一次后就不会继续执行了。ScheduledThreadPoolExecutor
继承了ThreadPoolExecutor
,所以其也是复用了ThreadPoolExecutor
的那一套逻辑。
使用CompletableFuture
的runAsync
提交任务,底层是通过ForkJoinPool
线程池进行执行,任务会被包装成AsyncRun
,且会返回一个CompletableFuture
给主线程。当任务出现异常时,处理方式和ThreadPoolExecutor
的submit
类似,异常堆栈不会被打印。只有在CompletableFuture
上调用get
方法尝试获取结果时,异常才会被打印。
위 내용은 Java 스레드의 예외 처리 메커니즘은 무엇입니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!