子執行緒的異常處理 Thread.UncaughtExceptionHandler
這是Thread類的一個內部類,它是一個介面
相關內容
實現類:
- ThreadGroup
概述
當執行緒因為一個沒有catch到的異常而終止時,可以通過這個介面的實現類處理一些後續工作。
當執行緒因為異常終止時,JVM會查詢這個執行緒的UncaughtExceptionHandler物件,並且呼叫handler's uncaughtException的方法,將thread物件和exception異常物件作為引數傳遞進去,如果Thread沒有顯示的setUncaughtExceptionHandler,那麼這個執行緒的ThreadGroup會做為它的UncaughtExceptionHandler。如果你用的是預設的ThreadGroup,那麼ThreadGroup的uncaughtException其實呼叫了thread.getDefaultUncaughtExceptionHandler,最終執行的uncaughtException角色是預設的ExceptionHandler。
方法
void uncaughtException(Thread t,Throwable e)
如果執行緒由於異常而終止,此方法就會被JVM呼叫。此方法內部丟擲的任何異常都會被系統忽略。這個方法是子執行緒執行任務出錯後的救命稻草。
Parameters:
t - the thread
e - the exception
程式碼示例
場景
為了演示這個Handler的用法,我們可以思考一下這個Handler的使用場景。
例如,每個業務系統一般都會包含一個登陸服務,登陸服務大多是一個獨立的微服務。一般登入介面中都會做一些輔助操作,如記錄登入日誌等等,而且我們希望這類輔助操作在出現異常的情況下,不會阻斷登入主業務介面的正常返回
一個比較常規的做法是使用訊息中介軟體將登入成功的使用者資訊封裝成訊息傳送至遠端服務非同步處理。但是對於一箇中小規模的業務系統,你引入了訊息中介軟體,也就是MQ的話,會帶來不小的維護成本,俗稱高射炮打蚊子。還有一種比較通用的辦法是將登入成功的日誌訊息放入一個Java阻塞佇列,然後使用一個執行緒池分配有限的資源去慢慢地將日誌資訊寫入資料庫,這樣登入請求正常返回,記錄日誌讓執行緒池非同步的去處理,這樣你沒有引入第三方的中介軟體,維護成本也不高,你只要學好Java就行了。
這種方式雖然很方便,但是其實存在問題:
寫入資料庫異常會導致丟訊息
在進行資料庫插入操作時,可能是資料庫連線池資源不夠,獲取連線超時會丟擲異常,此時執行這個任務的Thread就會終止,這條訊息就等於丟掉了,導致你的登入日誌記錄不準確。為了避免這個問題,我們可以使用Thread.UncaughtExceptionHandler在丟擲異常後將日誌記錄再次放回阻塞佇列
如果資料庫連線池資源緊張的問題只是瞬時的,那這樣ok,否則,還是會存在問題:
如果資料庫連線池資源長時間緊張,執行緒入庫持續拋異常、不停的將任務放回佇列,而且登陸介面壓力依然很高,導致佇列消費速度跟不上佇列增長速度,那麼:
如果使用有界佇列做日誌記錄緩衝,佇列會滿,日誌記錄無法放入佇列
如果使用無界佇列做日誌記錄緩衝,佇列可能資料量過大導致記憶體被撐爆
那麼我們該怎麼辦呢?
這個其實還是要看具體的需求,如果詳細的登入日誌對你來說沒有那麼重要,那麼你可以通過Thread.UncaughtExceptionHandler把登入日誌放入另一個佇列,然後定期對資料進行歸併,因為登入次數這種資料每個系統幾乎都要統計的。
如果你不光要統計登入人數,具體誰登入了你也要知道,那你可以提前準備一個備用的資料庫,在主資料庫壓力拉滿的情況下將資料放入另一個佇列寫入備用資料庫中。
其實Thread.UncaughtExceptionHandler並不是處理這種情況的最佳辦法,而且JDK不推薦我們顯示的建立執行緒,多數情況下最好使用執行緒池。不過本文的重點是介紹Thread.UncaughtExceptionHandler,並且加深記憶。
另外,這種基於JDK記憶體佇列的非同步模式在服務掛掉的時候會導致訊息大面積丟失,如果讀者真的會遇到此類場景,而且你的使用者量每日都有大量增長,建議直接使用MQ。只有使用者量穩定,而且你能保證系統壓力峰值絕對不會超過某個預估值時才推薦使用記憶體佇列。
相關程式碼
我們來寫寫程式碼實現上述上述場景
首先,我們先建立一個實體類,它代表登入日誌
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 登入日誌物件,用於記錄使用者登入資訊
* @author zhuzh
* @date 2019.10.17
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginLogPO {
private int id;
private String username;
private String createTime;
}
複製程式碼
有了實體類,繼續寫DAO層的介面
/**
* 登入日誌持久層
* @author zhuzh
* @date 2019.10.17
*/
public interface LoginLoggingDAO {
/**
* 記錄登入日誌到資料庫
* @param po
* @return
*/
boolean log(LoginLogPO po);
/**
* 記錄日誌失敗
* @return
*/
void logFailed();
/**
* 獲取備用資料來源中的日誌資料量
* @return
*/
int getBackupDatasourceSize();
}
複製程式碼
DAO層的模擬實現類,我們不會真的去插入資料庫,而是將資料放入一個佇列
/**
* 一個登入日誌持久層的模擬實現類
* @author zhuzh
* @date 2019.10.17
*/
public class MockLoginLoggingDAOImpl implements LoginLoggingDAO {
/**
* 使用定長佇列來模擬資料庫連線池資源緊張的場景
* MAIN_DATASOURCE代表主資料來源
* 建立一個長度為5的佇列,佇列滿了之後,向佇列插入資料會阻塞,直到佇列有空閒位置才可插入。
*/
private static final ArrayBlockingQueue<LoginLogPO> MAIN_DATASOURCE = new ArrayBlockingQueue<>(5);
/**
* 類初始化時直接將MAIN_DATASOURCE塞滿,這樣執行緒再插入資料時就會丟擲異常
*/
static {
System.out.println("第三步:將主資料來源塞滿,模擬主資料來源連線池資源不足");
for (int i = 0; i < 5; i++) {
MAIN_DATASOURCE.offer(new LoginLogPO(i,"使用者"+i,"2019-10-1"+i));
}
}
/**
* BACKUP_DATASOURCE代表備用資料來源,主資料來源不可用時程式會切換到備用資料來源,長度為100
*/
private static final ArrayBlockingQueue<LoginLogPO> BACKUP_DATASOURCE = new ArrayBlockingQueue<>(100);
private static final AtomicInteger FAILED_COUNTER= new AtomicInteger();
private static AtomicBoolean USE_BACKUP = new AtomicBoolean();
@Override
public boolean log(LoginLogPO po) {
ArrayBlockingQueue<LoginLogPO> currentDatasource = getDatasource();
try {
boolean success = currentDatasource.offer(po,3,TimeUnit.SECONDS);
if (!success){
throw new DatasourceBusyException("主資料來源繁忙,即將切換備用資料來源!");
}
System.out.println("第五步:入庫成功,入庫成功的資料來源為,currentDatasource="+(currentDatasource==BACKUP_DATASOURCE?"BACKUP_DATASOURCE":"MAIN_DATASOURCE"));
}catch (InterruptedException e){
e.printStackTrace();
return false;
}
return true;
}
@Override
public void logFailed() {
//記錄日誌的失敗次數+1
int currentFailedCount = FAILED_COUNTER.incrementAndGet();
//如果已經失敗10次了,說明現在主資料來源狀態不是很理想啊
if (currentFailedCount>=10&&!USE_BACKUP.get()){
//就啟用備用資料來源
USE_BACKUP.compareAndSet(false,true);
System.out.println("主資料來源繁忙,已切換資料來源為備資料來源,當前記錄失敗次數="+currentFailedCount+",備用資料來源標誌位="+USE_BACKUP.get()+",將使用備用資料來源");
}
}
/**
* 獲取資料來源
* @return
*/
private ArrayBlockingQueue<LoginLogPO> getDatasource(){
//如果主資料來源佇列未滿,說明主資料庫連線池資源充足,可以切回主資料來源
if (MAIN_DATASOURCE.isEmpty()){
FAILED_COUNTER.set(0);
USE_BACKUP.compareAndSet(true,false);
}
if (USE_BACKUP.get()){
return BACKUP_DATASOURCE;
}
return MAIN_DATASOURCE;
}
@Override
public int getBackupDatasourceSize() {
return BACKUP_DATASOURCE.size();
}
}
複製程式碼
持久層寫好了,我們還要寫一個Bean工廠才能通過靜態方法獲取一個單例的LoginLoggingDAO
/**
* Bean工廠,用於獲取日誌記錄的DAO
* @author zhuzh
* @date 2019.10.17
*/
public class LoginLogBeanFactory {
private LoginLogBeanFactory(){}
private static final Object locker = new Object();
private static LoginLoggingDAO loginLoggingDAO;
public static LoginLoggingDAO getInstance(){
if (loginLoggingDAO == null){
synchronized (locker){
loginLoggingDAO = new MockLoginLoggingDAOImpl();
}
}
return loginLoggingDAO;
}
}
複製程式碼
當主資料來源連線超時,我們要丟擲一種特定的異常來做標記,便於handler識別當前狀況
/**
* 使用特定異常標明當前確實是資料來源的問題
* @author zhuzh
* @date 2019.10.17
*/
public class DatasourceBusyException extends RuntimeException {
public DatasourceBusyException(String message) {
super(message);
}
}
複製程式碼
執行緒的執行任務
/**
* 記錄登入日誌的執行任務
* @author zhuzh
* @date 2019.10.17
*/
public class LoginLoggingTask implements Runnable {
private LoginLogPO po;
public LoginLoggingTask(LoginLogPO po1){
po = po1;
}
@Override
public void run() {
LoginLoggingDAO dao = LoginLogBeanFactory.getInstance();
dao.log(po);
}
@Override
public String toString() {
return "LoginLoggingTask{" +
"po=" + po +
'}';
}
}
複製程式碼
自定義的執行緒
/**
* 一個用來非同步記錄登入日誌的執行緒
* @author zhuzh
* @date 2019.10.17
*/
public class LoginLoggerThread extends Thread {
/**
* 執行任務暫存
*/
private Runnable task;
/**
* 此方法將runnable執行任務賦值給類的成員變數task,便於後續如果出現異常時可以重新執行任務
* @param target
*/
public LoginLoggerThread(Runnable target) {
super(target);
task = target;
}
public Runnable getTask() {
return task;
}
}
複製程式碼
和UncaughtExceptionHandler
/**
* 一個自定義的執行緒任務異常處理器
* @author zhuzh
* @date 2019.10.17
*/
public class LoginLoggingUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t,Throwable e) {
//如果確實是資料來源的問題,我們再切換資料來源,如果丟擲了別的異常,暫不處理
if (e instanceof DatasourceBusyException){
LoginLoggingDAO dao = LoginLogBeanFactory.getInstance();
dao.logFailed();
LoginLoggerThread thread = (LoginLoggerThread)t;
Runnable task = thread.getTask();
System.out.println("第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task="+task.toString());
AsynLogger.log(task);
}
}
}
複製程式碼
接下來我們再寫一個非同步日誌處理類,接收日誌記錄到緩衝佇列
/**
* 一個登陸日誌非同步處理類
* @author zhuzh
* @date 2019.10.17
*/
public class AsynLogger {
/**
* 日誌緩衝佇列,當使用者登入成功時,通過呼叫AsynLogger.log將日誌放入緩衝佇列
*/
private static final ArrayBlockingQueue<Runnable> TASKS = new ArrayBlockingQueue<>(50);
/**
* 初始化50個使用者登入日誌
*/
static {
System.out.println("第一步:初始化50條登入日誌資料放入[日誌緩衝佇列TASKS]");
for (int i = 0; i < 50; i++) {
TASKS.offer(new LoginLoggingTask(new LoginLogPO(i,"2019-10-1"+i)));
}
}
public static void log(Runnable loggingTask){
TASKS.offer(loggingTask);
}
/**
* 呼叫startLogging方法啟動一個執行緒輪訓緩衝佇列
*/
public static void startLogging(){
Thread a = new Thread(()->{
while (true){
Runnable task = TASKS.poll();
if (task==null){
continue;
}
LoginLoggerThread thread = new LoginLoggerThread(task);
thread.setUncaughtExceptionHandler(new LoginLoggingUncaughtExceptionHandler());
thread.start();
}});
a.setDaemon(true);
a.start();
System.out.println("第二步:建立獨立守護執行緒輪訓[日誌緩衝佇列TASKS],如果有資料,建立一個執行緒去處理,並且設定UncaughtExceptionHandler");
}
}
複製程式碼
最後我們再寫一個main方法,get it done!
/**
* 測試功能主類
* @author zhuzh
* @date 2019.10.17
*/
public class UncaughtExceptionHandlerExample {
public static void main(String[] args){
AsynLogger.startLogging();
try {
Thread.sleep(10000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("第六步:所有任務執行完畢,備用資料來源有"+ LoginLogBeanFactory.getInstance().getBackupDatasourceSize()+"條資料");
}
}
複製程式碼
輸出結果
第一步:初始化50條登入日誌資料放入[日誌緩衝佇列TASKS]
第二步:建立獨立守護執行緒輪訓[日誌緩衝佇列TASKS],如果有資料,建立一個執行緒去處理,並且設定UncaughtExceptionHandler
第三步:將主資料來源塞滿,模擬主資料來源連線池資源不足
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=33,username=使用者33,createTime=2019-10-133)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=38,username=使用者38,createTime=2019-10-138)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=5,username=使用者5,createTime=2019-10-15)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=39,username=使用者39,createTime=2019-10-139)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=34,username=使用者34,createTime=2019-10-134)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=36,username=使用者36,createTime=2019-10-136)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=47,username=使用者47,createTime=2019-10-147)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=37,username=使用者37,createTime=2019-10-137)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=8,username=使用者8,createTime=2019-10-18)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=46,username=使用者46,createTime=2019-10-146)}
主資料來源繁忙,已切換資料來源為備資料來源,當前記錄失敗次數=10,備用資料來源標誌位=true,將使用備用資料來源
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=23,username=使用者23,createTime=2019-10-123)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=40,username=使用者40,createTime=2019-10-140)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=13,username=使用者13,createTime=2019-10-113)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=17,username=使用者17,createTime=2019-10-117)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=45,username=使用者45,createTime=2019-10-145)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=15,username=使用者15,createTime=2019-10-115)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=44,username=使用者44,createTime=2019-10-144)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=12,username=使用者12,createTime=2019-10-112)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=31,username=使用者31,createTime=2019-10-131)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=18,username=使用者18,createTime=2019-10-118)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=35,username=使用者35,createTime=2019-10-135)}
第五步:入庫成功,入庫成功的資料來源為,currentDatasource=BACKUP_DATASOURCE
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=43,username=使用者43,createTime=2019-10-143)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=29,username=使用者29,createTime=2019-10-129)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=30,username=使用者30,createTime=2019-10-130)}
第五步:入庫成功,入庫成功的資料來源為,currentDatasource=BACKUP_DATASOURCE
第五步:入庫成功,入庫成功的資料來源為,task=LoginLoggingTask{po=LoginLogPO(id=19,username=使用者19,createTime=2019-10-119)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=16,username=使用者16,createTime=2019-10-116)}
第五步:入庫成功,入庫成功的資料來源為,task=LoginLoggingTask{po=LoginLogPO(id=7,username=使用者7,createTime=2019-10-17)}
第五步:入庫成功,入庫成功的資料來源為,task=LoginLoggingTask{po=LoginLogPO(id=27,username=使用者27,createTime=2019-10-127)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=11,username=使用者11,createTime=2019-10-111)}
第五步:入庫成功,入庫成功的資料來源為,task=LoginLoggingTask{po=LoginLogPO(id=6,username=使用者6,createTime=2019-10-16)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=4,username=使用者4,createTime=2019-10-14)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=28,username=使用者28,createTime=2019-10-128)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=3,username=使用者3,createTime=2019-10-13)}
第五步:入庫成功,入庫成功的資料來源為,task=LoginLoggingTask{po=LoginLogPO(id=20,username=使用者20,createTime=2019-10-120)}
第五步:入庫成功,入庫成功的資料來源為,task=LoginLoggingTask{po=LoginLogPO(id=2,username=使用者2,createTime=2019-10-12)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=41,username=使用者41,createTime=2019-10-141)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=9,username=使用者9,createTime=2019-10-19)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=22,username=使用者22,createTime=2019-10-122)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=1,username=使用者1,createTime=2019-10-11)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=0,username=使用者0,createTime=2019-10-10)}
第五步:入庫成功,入庫成功的資料來源為,task=LoginLoggingTask{po=LoginLogPO(id=25,username=使用者25,createTime=2019-10-125)}
第五步:入庫成功,入庫成功的資料來源為,task=LoginLoggingTask{po=LoginLogPO(id=42,username=使用者42,createTime=2019-10-142)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=48,username=使用者48,createTime=2019-10-148)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=14,username=使用者14,createTime=2019-10-114)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=49,username=使用者49,createTime=2019-10-149)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=26,username=使用者26,createTime=2019-10-126)}
第五步:入庫成功,入庫成功的資料來源為,task=LoginLoggingTask{po=LoginLogPO(id=24,username=使用者24,createTime=2019-10-124)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=21,username=使用者21,createTime=2019-10-121)}
第五步:入庫成功,入庫成功的資料來源為,task=LoginLoggingTask{po=LoginLogPO(id=10,username=使用者10,createTime=2019-10-110)}
第四步:執行緒執行異常,進入UncaughtExceptionHandler,任務重新丟回任務佇列,task=LoginLoggingTask{po=LoginLogPO(id=32,username=使用者32,createTime=2019-10-132)}
第五步:入庫成功,入庫成功的資料來源為,currentDatasource=BACKUP_DATASOURCE
第六步:所有任務執行完畢,備用資料來源有50條資料
Process finished with exit code 0
複製程式碼
其實這種每次起一個執行緒去處理任務的方式是不正確的,應該用執行緒池,但是本文主要目的是講解Thread的內部介面UncaughtExceptionHandler,後面我會用更好的方案來修改這個例子
本文的原始碼在github上,可以到這裡下載