1. 程式人生 > >七、JAVA多執行緒:Hook執行緒以及捕獲執行緒執行異常(UncaughtExceptionHandler、Hook)

七、JAVA多執行緒:Hook執行緒以及捕獲執行緒執行異常(UncaughtExceptionHandler、Hook)

        本章將介紹,如何獲取執行緒在執行時期的異常,以及如何向JAVA程式注入Hook執行緒。

獲取執行緒執行時異常

在Thread類中,關於處理執行時異常的API一共有四個。

1.為某個特定執行緒指定UncaughtExceptionHandler

public void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)

2.設定全域性的UncaughtExceptionHandler

public static Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler()

3.獲取特定執行緒的UncaughtExceptionHandler

public Thread.UncaughtExceptionHandler getUncaughtExceptionHandler()

4.獲取全域性的UncaughtExceptionHandler

public static Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler()

 

UncaughtExceptionHandler 介紹

 

執行緒在執行單元中不允許丟擲checked異常,而且執行緒執行在自己的上下文中,派生它的執行緒無法直接獲得它執行中出現的異常資訊。對此,Java為我們提供了UncaughtExceptionHandler介面,當執行緒在執行過程中出現異常時,會回撥UncaughtExceptionHandler介面,從而得知是哪個執行緒在執行時出錯。UncaughtExceptionHandler介面在Thread中定義。

 

 

/**
 * Interface for handlers invoked when a <tt>Thread</tt> abruptly
 * terminates due to an uncaught exception.
 * <p>When a thread is about to terminate due to an uncaught exception
 * the Java Virtual Machine will query the thread for its
 * <tt>UncaughtExceptionHandler</tt> using
 * {@link #getUncaughtExceptionHandler} and will invoke the handler's
 * <tt>uncaughtException</tt> method, passing the thread and the
 * exception as arguments.
 * If a thread has not had its <tt>UncaughtExceptionHandler</tt>
 * explicitly set, then its <tt>ThreadGroup</tt> object acts as its
 * <tt>UncaughtExceptionHandler</tt>. If the <tt>ThreadGroup</tt> object
 * has no
 * special requirements for dealing with the exception, it can forward
 * the invocation to the {@linkplain #getDefaultUncaughtExceptionHandler
 * default uncaught exception handler}.
 *
 * @see #setDefaultUncaughtExceptionHandler
 * @see #setUncaughtExceptionHandler
 * @see ThreadGroup#uncaughtException
 * @since 1.5
 */
@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);
}

 

UncaughtExceptionHandler是一個FunctionalInterface ,只有一個抽象方法,

該回調介面會被Thread中的dispatchUncaughtException呼叫

/**
 * Dispatch an uncaught exception to the handler. This method is
 * intended to be called only by the JVM.
 */
private void dispatchUncaughtException(Throwable e) {
    getUncaughtExceptionHandler().uncaughtException(this, e);
}

當執行緒在執行過程中出現異常時,JVM會呼叫dispatchUncaughtException方法,

該方法會將對應的執行緒例項以及異常資訊傳遞給回撥介面

 

UncaughtExceptionHandler樣例

package com.zl.step7;

import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;

import java.util.concurrent.TimeUnit;

public class CaptureThreadExeception {
    public static void main(String[] args) {
        Thread.setDefaultUncaughtExceptionHandler((t,e) -> {
            System.out.println(t.getName() + " occur exception");

            e.printStackTrace();

        });

        // 這裡將會出現unchecked異常
        final Thread thread = new Thread(() -> {

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }


            System.out.println(1/0);

        }, "Test-Thread ") ;

        thread.start();

    }
}

 

 

返回異常:

Connected to the target VM, address: '127.0.0.1:60373', transport: 'socket'
Disconnected from the target VM, address: '127.0.0.1:60373', transport: 'socket'
Test-Thread  occur exception
java.lang.ArithmeticException: / by zero
    at com.zl.step7.CaptureThreadExeception.lambda$main$1(CaptureThreadExeception.java:28)
    at java.lang.Thread.run(Thread.java:748)

Process finished with exit code 0
 

 

UncaughtExceptionHandler原始碼分析

 

 

/**
 * Returns the handler invoked when this thread abruptly terminates
 * due to an uncaught exception. If this thread has not had an
 * uncaught exception handler explicitly set then this thread's
 * <tt>ThreadGroup</tt> object is returned, unless this thread
 * has terminated, in which case <tt>null</tt> is returned.
 * @since 1.5
 * @return the uncaught exception handler for this thread
 */
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
    return uncaughtExceptionHandler != null ?
        uncaughtExceptionHandler : group;
}

 

getUncaughtExceptionHandler 方法首先會判斷當前執行緒是否設定了handler,如果有則執行自己的uncaughtExceptionHandler

否者就到ThreadGroup中獲取。

ThreadGroup中的uncaughtExceptionHandler 方法:

/**
 * Called by the Java Virtual Machine when a thread in this
 * thread group stops because of an uncaught exception, and the thread
 * does not have a specific {@link Thread.UncaughtExceptionHandler}
 * installed.
 * <p>
 * The <code>uncaughtException</code> method of
 * <code>ThreadGroup</code> does the following:
 * <ul>
 * <li>If this thread group has a parent thread group, the
 *     <code>uncaughtException</code> method of that parent is called
 *     with the same two arguments.
 * <li>Otherwise, this method checks to see if there is a
 *     {@linkplain Thread#getDefaultUncaughtExceptionHandler default
 *     uncaught exception handler} installed, and if so, its
 *     <code>uncaughtException</code> method is called with the same
 *     two arguments.
 * <li>Otherwise, this method determines if the <code>Throwable</code>
 *     argument is an instance of {@link ThreadDeath}. If so, nothing
 *     special is done. Otherwise, a message containing the
 *     thread's name, as returned from the thread's {@link
 *     Thread#getName getName} method, and a stack backtrace,
 *     using the <code>Throwable</code>'s {@link
 *     Throwable#printStackTrace printStackTrace} method, is
 *     printed to the {@linkplain System#err standard error stream}.
 * </ul>
 * <p>
 * Applications can override this method in subclasses of
 * <code>ThreadGroup</code> to provide alternative handling of
 * uncaught exceptions.
 *
 * @param   t   the thread that is about to exit.
 * @param   e   the uncaught exception.
 * @since   JDK1.0
 */
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);
        }
    }
}

 

總結:

1.如果ThreadGroup 如果有父ThreadGroup ,則直接呼叫父Group的uncaughtException方法

2.如果設定了全域性預設UncaughtExceptionHandler ,則呼叫uncaughtException 方法

3.若沒有父的ThreadGroup 沒有全域性預設的UncaughtExceptionHandler ,

    直接將異常的堆疊資訊定向到System.err中

 

package com.zl.step7;

import java.util.concurrent.TimeUnit;

public class EmptyExceptionHandler {

    public static void main(String[] args) {
        ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();


        System.out.println(mainGroup.getName());
        System.out.println(mainGroup.getParent());
        System.out.println(mainGroup.getParent().getParent());

        final Thread thread = new Thread(()-> {

            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(1/0);

        },"Test-Thread");

        thread.start();

    }


}

 

 

 

注入鉤子執行緒

Java程式經常也會遇到程序掛掉的情況,一些狀態沒有正確的儲存下來,這時候就需要在JVM關掉的時候執行一些清理現場的程式碼。JAVA中的ShutdownHook提供了比較好的方案。

JDK提供了Java.Runtime.addShutdownHook(Thread hook)方法,可以註冊一個JVM關閉的鉤子,這個鉤子可以在一下幾種場景中被呼叫:

程式正常退出
使用System.exit()
終端使用Ctrl+C觸發的中斷
系統關閉
OutOfMemory宕機
使用Kill pid命令幹掉程序(注:在使用kill -9 pid時,是不會被呼叫的)

 

程式碼樣例

import java.util.concurrent.TimeUnit;

public class ThreadHook {
    public static void main(String[] args) {


        // 注入鉤子執行緒
        Runtime.getRuntime().addShutdownHook(new Thread(){

            @Override
            public void run(){

                System.out.println("The hook thread 1 is running ....");

                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }


                System.out.println("The hook thread 1 is exit  ....");

            }


        });



        // 鉤子執行緒可以註冊多個
        Runtime.getRuntime().addShutdownHook(new Thread(){

            @Override
            public void run(){

                System.out.println("The hook thread 2 is running ....");

                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }


                System.out.println("The hook thread 2 is exit  ....");

            }


        });


        System.out.println(" The program will is stopping . ");

    }
}

 

 

輸出結果:

The program will is stopping . 
The hook thread 1 is running ....
The hook thread 2 is running ....
The hook thread 1 is exit  ....
Disconnected from the target VM, address: '127.0.0.1:60787', transport: 'socket'
The hook thread 2 is exit  ....

Process finished with exit code 0

 

Hook執行緒實戰

        在開發的過程中經常會遇到Hook執行緒,比如為了防止某個程式被重複啟動,在程序啟動的時候,會建立一個lock檔案,程序收到中斷訊號的時候會刪除這個lock檔案,我們在mysql、zookeeper、kafka等系統中都會看到lock檔案的存在。

 

模擬一個防止重複啟動的程式:

 

package com.zl.step7;

import com.sun.tools.javac.code.Type;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Set;
import java.util.concurrent.TimeUnit;

public class PreventDuplicated {

    private final  static String LOCK_PATH = "/A/" ;

    private final  static String LOCK_FILE = ".lock" ;

    private final static String   PERMISSIONS = "rw-------" ;

    public static void main(String[] args) throws IOException {

        Runtime.getRuntime().addShutdownHook(new Thread(()->{
            System.out.println("The program received kill SIGNAL . ");
            getLockFile().toFile().delete();
        }));


       checkRunning();

       for (;;) {

           try {
               TimeUnit.SECONDS.sleep(1);

               System.out.println("program is running.");
               
           } catch (InterruptedException e) {
               e.printStackTrace();
           }

       }

    }

    private static void checkRunning() throws IOException {
        Path path = getLockFile();
        if(path.toFile().exists()){
            throw new RuntimeException("The program is already running .") ;
        }

        Set<PosixFilePermission> perms = PosixFilePermissions.fromString(PERMISSIONS) ;

       Files.createFile(path, PosixFilePermissions.asFileAttribute(perms));

    }


    private static Path getLockFile(){
        return Paths.get(LOCK_PATH,LOCK_FILE) ;
    }
}

 

線上程執行的時候在 /A路徑下 會生成一個 .lock 檔案

當殺掉程序之後,就會將該檔案刪除。

 

Hook執行緒應用場景,以及注意事項。

 

1.Hook執行緒只有在收到退出訊號的時候才會被執行,如果在kill的時候,使用了引數 -9 , 

那麼Hook執行緒不會得到執行,程序將立即退出,因此.lock檔案將得不到清理。

2.Hook執行緒中也可以執行一些釋放資源的操作,比如關閉檔案控制代碼,socket連線,資料庫connection等

3.儘量不要在Hook執行緒中執行一些耗時非常長的操作,因為其會導致程式遲遲不能退出。

4.如果強制殺死程序,那麼程序間更不會收到任何中斷訊號。

 

 

 

 

 

 

 

 

 

本文來源於:

《Java高併發程式設計詳解:多執行緒與架構設計》 --汪文君