七、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高併發程式設計詳解:多執行緒與架構設計》 --汪文君