Rumah  >  Artikel  >  Java  >  Apakah mekanisme pengendalian pengecualian dalam benang Java?

Apakah mekanisme pengendalian pengecualian dalam benang Java?

WBOY
WBOYke hadapan
2023-04-21 21:37:061158semak imbas

    Preface

    Memulakan program Java pada asasnya menjalankan kaedah utama kelas Java. Kami menulis program gelung tak terhingga, jalankannya, dan kemudian jalankan jvisualvm untuk memerhati

    Apakah mekanisme pengendalian pengecualian dalam benang Java?

    Anda boleh melihat bahawa terdapat 11 utas dalam proses Java ini, 10 daripadanya adalah penjaga, 1 utas pengguna. Kod dalam kaedah utama kami dijalankan dalam urutan bernama main. Apabila semua utas yang berjalan dalam proses Java adalah utas daemon, JVM akan keluar .

    Dalam senario satu benang, jika pengecualian dilemparkan apabila kod berjalan ke lokasi tertentu, anda akan melihat maklumat tindanan pengecualian dicetak pada konsol. Walau bagaimanapun, dalam senario berbilang benang, pengecualian yang berlaku dalam sub-benang mungkin tidak semestinya mencetak maklumat pengecualian tepat pada masanya.

    Saya pernah menemuinya di tempat kerja Apabila menggunakan CompletableFuture.runAsync untuk memproses tugasan yang memakan masa secara tidak segerak, pengecualian berlaku semasa pemprosesan tugasan, tetapi tiada maklumat tentang pengecualian dalam log. Selepas sekian lama, saya menyemak semula mekanisme pengendalian pengecualian dalam utas dan mendalami pemahaman saya tentang prinsip kerja utas, saya dengan ini merekodkannya.

    Mekanisme pengendalian pengecualian bagi benang

    Kami tahu bahawa untuk menjalankan program Java, kod sumber Java mula-mula disusun ke dalam fail bytecode kelas melalui javac, dan kemudian JVM memuatkan dan menghuraikan fail kelas, dan kemudian memulakan pelaksanaan daripada kaedah utama kelas utama. Apabila benang melontar pengecualian tidak ditangkap semasa operasi, JVM akan memanggil kaedah dispatchUncaughtException pada objek benang untuk pengendalian pengecualian. Kod sumber

    // Thread类中
    private void dispatchUncaughtException(Throwable e) {
            getUncaughtExceptionHandler().uncaughtException(this, e);
    }

    mudah difahami, mula-mula dapatkan UncaughtExceptionHandler pengendali pengecualian, dan kemudian kendalikan pengecualian dengan memanggil kaedah uncaughtException pengendali pengecualian ini. (Singkatan ueh digunakan di bawah untuk mewakili UncaughtExceptionHandler) Apakah itu

    ueh? Malah, ia adalah antara muka yang ditakrifkan di dalam Thread untuk pengendalian pengecualian.

        @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);
        }

    Mari kita lihat kaedah Thread dalam objek getUncaughtExceptionHandler

    	public UncaughtExceptionHandler getUncaughtExceptionHandler() {
            return uncaughtExceptionHandler != null ?
                uncaughtExceptionHandler : group;
        }

    Mula-mula semak sama ada objek Thread semasa mempunyai ueh tersuai. set objek. Jika ya, pengecualian akan dikendalikan olehnya, jika tidak, kumpulan benang (Thread) yang dimiliki oleh objek ThreadGroup semasa akan mengendalikan pengecualian. Apabila kita mengklik pada kod sumber terbuka, kita boleh mendapati dengan mudah bahawa kelas ThreadGroup itu sendiri melaksanakan antara muka Thread.UncaughtExceptionHandler, yang bermaksud bahawa ThreadGroup itu sendiri ialah pengendali pengecualian.

    public class ThreadGroup implements Thread.UncaughtExceptionHandler {
        private final ThreadGroup parent;
        ....
    }

    Katakan kita membuang pengecualian dalam kaedah main Jika tiada objek main tersuai yang ditetapkan untuk utas ueh, ia akan diserahkan kepada main yang kepadanya. benang ThreadGroup kepunyaan untuk mengendalikan pengecualian. Mari kita lihat cara ThreadGroup mengendalikan pengecualian:

        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);
                }
            }
        }

    Bahagian kod sumber ini juga agak pendek. Yang pertama ialah untuk menyemak sama ada ThreadGroup semasa mempunyai ibu bapa ThreadGroup Jika ya, hubungi ibu bapa ThreadGroup untuk pengendalian pengecualian. Jika tidak, panggil kaedah statik Thread.getDefaultUncaughtExceptionHandler() untuk mendapatkan objek lalai ueh.

    Jika objek lalai tidak kosong, objek ueh lalai ini akan melakukan pengendalian pengecualian jika tidak, apabila pengecualian bukan ueh, ThreadDeathNama urutan semasa dan maklumat tindanan pengecualian , dicetak ke konsol melalui output ralat standard (). System.err

    Kami hanya menjalankan kaedah

    dan lihat status urutan main

    Apakah mekanisme pengendalian pengecualian dalam benang Java?

    Apakah mekanisme pengendalian pengecualian dalam benang Java?

    Anda boleh lihat,

    Benang itu kepunyaan main juga bernama main dan ThreadGroup ini main mempunyai induknya ThreadGroup bernama ThreadGroup dan system ini system tidak mempunyai ibu bapa , ia adalah punca ThreadGroup. ThreadGroup

    Dapat dilihat bahawa pengecualian tidak ditangkap yang dilemparkan dalam benang

    akhirnya akan diserahkan kepada main bernama system untuk pengendalian pengecualian, dan kerana tiada ThreadGroup lalai tetapkan objek , maklumat pengecualian akan dikeluarkan kepada konsol melalui ueh. System.err

    Seterusnya, kami menggunakan cara paling mudah (

    a new) untuk mencipta sub-benang dalam Thread benang, menulis kod yang boleh membuang pengecualian dalam sub-benang dan perhatikan main

        public static void main(String[] args)  {
            Thread thread = new Thread(() -> {
                System.out.println(3 / 0);
            });
            thread.start();
        }

    Apakah mekanisme pengendalian pengecualian dalam benang Java?

    子线程中的异常信息被打印到了控制台。异常处理的流程就是我们上面描述的那样。

    小结

    所以,正常来说,如果没有对某个线程设置特定的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方法中,会执行到下图红框的部分

    Apakah mekanisme pengendalian pengecualian dalam benang Java?

    Apakah mekanisme pengendalian pengecualian dalam benang Java?

    在上面的代码执行完毕后,由于异常被throw了出来,所以会由JVM捕捉到,并调用当前子线程dispatchUncaughtException方法进行处理,根据上面的分析,最终异常堆栈会被打印到控制台。

    多扯几句别的。

    上面跟源码时,注意到WorkerThreadPoolExecutor的一个内部类,也就是说,每个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方法

    Apakah mekanisme pengendalian pengecualian dalam benang Java?

    再次跟源码时,加深了对ThreadPoolExecutorWorker体系的理解和认识。

    它们之间有一种嵌套依赖的关系。每个Worker里持有一个Thread对象,这个Thread对象又是以这个Worker对象作为Runnable,而Worker又是ThreadPoolExecutor的内部类,这意味着每个Worker对象都会隐式的持有其所属的ThreadPoolExecutor对象的引用。每个Workerrun方法, 都跑在子线程中,但是这些Worker跑在子线程中时,能够对ThreadPoolExecutor对象的属性进行访问和修改(每个Workerrun方法都是调用的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,又实现了RunnableFuture接口

    Apakah mekanisme pengendalian pengecualian dalam benang Java?

    于是我们看下FutureTaskrun方法(因为最终是将包装后的FutureTask提交给线程池执行,所以最终会执行FutureTaskrun方法)

    Apakah mekanisme pengendalian pengecualian dalam benang Java?

        protected void setException(Throwable t) {
            if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
                outcome = t;
                UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
                finishCompletion();
            }
        }

    可以看到,异常信息只是被简单的设置到了FutureTaskoutcome字段上。并没有往外抛,所以这里其实相当于把异常给生吞了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); // 异常会通过这一句被抛出来
        }

    小结

    • 通过ThreadPoolExecutorexecute方法提交的任务,出现异常后,异常会在子线程中被抛出,并被JVM捕获,并调用子线程的dispatchUncaughtException方法,进行异常处理,若子线程没有任何特殊设置,则异常堆栈会被输出到System.err,即异常会被打印到控制台上。并且会从线程池中移除当前Worker,并另启一个新的Worker作为替代。

    • 通过ThreadPoolExecutorsubmit方法提交的任务,任务会先被包装成FutureTask对象,出现异常后,异常会被生吞,并暂存到FutureTask对象中,作为任务执行结果的一部分。异常信息不会被打印该子线程也不会被线程池移除(因为异常在子线程中被吞了,没有抛出来)。在调用FutureTask上的get方法时(此时一般是在主线程中了),异常才会被抛出,触发主线程的异常处理,并输出到System.err

    其他

    其他的线程池场景

    比如:

    • 使用ScheduledThreadPoolExecutor实现延迟任务或者定时任务(周期任务),分析过程也是类似。这里给个简单结论,当调用scheduleAtFixedRate方法执行一个周期任务时(任务会被包装成FutureTask (实际是ScheduledFutureTask ,是FutureTask 的子类)),若周期任务中出现异常,异常会被生吞,异常信息不会被打印,线程不会被回收,但是周期任务执行这一次后就不会继续执行了。ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,所以其也是复用了ThreadPoolExecutor的那一套逻辑。

    • 使用CompletableFuture runAsync 提交任务,底层是通过ForkJoinPool 线程池进行执行,任务会被包装成AsyncRun ,且会返回一个CompletableFuture 给主线程。当任务出现异常时,处理方式和ThreadPoolExecutor 的submit 类似,异常堆栈不会被打印。只有在CompletableFuture 上调用get 方法尝试获取结果时,异常才会被打印。

    Atas ialah kandungan terperinci Apakah mekanisme pengendalian pengecualian dalam benang Java?. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

    Kenyataan:
    Artikel ini dikembalikan pada:yisu.com. Jika ada pelanggaran, sila hubungi admin@php.cn Padam