1. 程式人生 > >更加強健的執行緒模型,解決執行緒卡死,退出異常情況

更加強健的執行緒模型,解決執行緒卡死,退出異常情況

執行緒模型

 1 package net.sz;
 2 
 3 import java.util.Random;
 4 import java.util.concurrent.ConcurrentLinkedQueue;
 5 import org.apache.log4j.Logger;
 6 
 7 /**
 8  *
 9  * <br>
10  * author 失足程式設計師<br>
11  * mail [email protected]<br>
12  * phone 13882122019<br>
13  */
14 public
class ThreadTest implements Runnable { 15 16 private static final Logger log = Logger.getLogger(ThreadTest.class); 17 18 public ConcurrentLinkedQueue<Runnable> runs = new ConcurrentLinkedQueue<>(); 19 20 Runnable run; 21 long lastTimer; 22 23 @Override 24 public
void run() { 25 while (true) { 26 run = null; 27 lastTimer = 0; 28 try { 29 /*如果佇列為空強制執行緒等待*/ 30 while (runs.isEmpty()) { 31 synchronized (runs) { 32 /*直到收到通知訊息*/ 33 runs.wait();
34 } 35 } 36 37 /*取出任務*/ 38 run = runs.poll(); 39 lastTimer = System.currentTimeMillis(); 40 if (run != null) { 41 /*執行任務*/ 42 run.run(); 43 } 44 } catch (Exception e) { 45 /*捕獲異常*/ 46 log.error("", e); 47 } 48 } 49 } 50 }

我相信這段程式碼,70%左右的java開發人員,線上程佇列執行的執行緒,執行緒模型都是這樣設計的

我也是,而且這樣的程式碼持續了很多年;

執行緒執行 Runnable 介面實現,內部封裝了任務列表,

執行緒執行的時候取出佇列裡面第一個,執行,

之所以加上開始執行時間就是為了檢查當前執行緒物件執行任務的時候卡死了,比如一直等待;

並且在死迴圈裡面添加了 try catch 捕獲異常現象;

看上去連執行緒退出的機會都沒有了是不是呢?

感覺沒啥問題啊, 接下來我們看看

執行緒模型使用

 1 public class TestMain {
 2 
 3     private static final Logger log = Logger.getLogger(TestMain.class);
 4     static ThreadTest threadTest = new ThreadTest();
 5 
 6     public static void main(String[] args) throws InterruptedException {
 7         /*建立執行緒任務佇列*/
 8         Thread thread = new Thread(threadTest);
 9         /*啟動執行緒*/
10         thread.start();
11         long i = 0;
12 
13         Random random = new Random();
14 
15         Thread thread1 = new Thread(new Runnable() {
16             @Override
17             public void run() {
18                 while (true) {
19                     try {
20                         /*相當於沒 2秒 有一個任務需要處理*/
21                         Thread.sleep(2000);
22                     } catch (Exception e) {
23                     }
24                     /*建立任務*/
25                     threadTest.runs.add(new Runnable() {
26                         @Override
27                         public void run() {
28                             int nextInt = random.nextInt(10000);
29                             if (nextInt < 2000) {
30                                 try {
31                                     /*相當於有20%的概率暫停 5秒 模擬任務的執行耗時*/
32                                     Thread.sleep(5000);
33                                 } catch (Exception e) {
34                                 }
35                             } else if (nextInt < 5000) {
36                                 try {
37                                     /*相當於有50%的概率暫停 2秒 模擬任務的執行耗時*/
38                                     Thread.sleep(2000);
39                                 } catch (Exception e) {
40                                 }
41                             }
42                             log.error(System.currentTimeMillis());
43                         }
44                     });
45                     /*通知執行緒有新任務了*/
46                     synchronized (threadTest.runs) {
47                         threadTest.runs.notify();
48                     }
49                 }
50             }
51         });
52         thread1.start();
53         while (true) {
54             try {
55                 /*相當於沒 1秒 檢查*/
56                 Thread.sleep(1000);
57             } catch (Exception e) {
58             }
59             long timer = System.currentTimeMillis() - threadTest.lastTimer;
60             if (threadTest.lastRun != null) {
61                 if (timer > 500) {
62                     log.error("執行緒可能已卡死:" + timer);
63                 } else {
64                     log.error("執行緒執行耗時:" + timer);
65                 }
66             }
67         }
68     }
69 }
View Code

以上程式碼,我們執行緒在處理佇列任務的時候使用的一般情況

[01-20 15:43:10:0544:ERROR: sz.TestMain.run():93 ] -> 1484898190544
[01-20 15:43:10:0544:ERROR: sz.TestMain.run():93 ] -> 1484898190544
[01-20 15:43:10:0557:ERROR: sz.TestMain.main():115] -> 執行緒執行耗時:13
[01-20 15:43:11:0557:ERROR: sz.TestMain.main():113] -> 執行緒可能已卡死:1013
[01-20 15:43:12:0544:ERROR: sz.TestMain.run():93 ] -> 1484898192544
[01-20 15:43:12:0558:ERROR: sz.TestMain.main():115] -> 執行緒執行耗時:14
[01-20 15:43:13:0558:ERROR: sz.TestMain.main():113] -> 執行緒可能已卡死:1014
[01-20 15:43:14:0545:ERROR: sz.TestMain.run():93 ] -> 1484898194545
[01-20 15:43:14:0545:ERROR: sz.TestMain.run():93 ] -> 1484898194545
[01-20 15:43:14:0545:ERROR: sz.TestMain.run():93 ] -> 1484898194545
[01-20 15:43:14:0545:ERROR: sz.TestMain.run():93 ] -> 1484898194545
[01-20 15:43:16:0559:ERROR: sz.TestMain.main():115] -> 執行緒執行耗時:13
[01-20 15:43:17:0559:ERROR: sz.TestMain.main():113] -> 執行緒可能已卡死:1013

這些都是我在模擬執行緒執行流程操作完全沒有問題,

可能很多看過我之前程式碼的童鞋也知道,我的執行緒模型一直都是這麼定義,這麼使用的;

並且使用了很多年,我有理由相信很多小夥伴都是這樣寫的!

如果是請請舉個爪。恩,扥,對,我是這樣的

好吧接下來我們改造一下拋錯異常,處理

執行緒執行任務的時候總有可能拋錯異常對不對;

丟擲異常的執行緒任務

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        /*相當於沒 2秒 有一個任務需要處理*/
                        Thread.sleep(2000);
                    } catch (Exception e) {
                    }
                    /*建立任務*/
                    threadTest.runs.add(new Runnable() {
                        @Override
                        public void run() {
                            int nextInt = random.nextInt(10000);
                            if (nextInt < 1000) {
                                try {
                                    /*相當於有10%的概率暫停 5秒 模擬任務的執行耗時*/
                                    Thread.sleep(5000);
                                } catch (Exception e) {
                                }
                            } else if (nextInt < 3000) {
                                try {
                                    /*相當於有30%的概率暫停 2秒 模擬任務的執行耗時*/
                                    Thread.sleep(2000);
                                } catch (Exception e) {
                                }
                            } else if (nextInt < 5000) {
                                throw new UnsupportedOperationException("模擬拋錯異常");
                            }
                            log.error(System.currentTimeMillis());
                        }
                    });
                    /*通知執行緒有新任務了*/
                    synchronized (threadTest.runs) {
                        threadTest.runs.notify();
                    }
                }
            }
        });
        thread1.start();

修改一下執行緒新增任務的程式碼模擬跑出異常

[01-20 15:49:08:0874:ERROR: sz.TestMain.run():95 ] -> 1484898548874
[01-20 15:49:08:0875:ERROR: sz.TestMain.run():46 ] -> 
java.lang.UnsupportedOperationException: 模擬拋錯異常
    at net.sz.TestMain$1$1.run(TestMain.java:93)
    at net.sz.ThreadTest.run(TestMain.java:42)
    at java.lang.Thread.run(Thread.java:745)
[01-20 15:49:09:0875:ERROR: sz.TestMain.run():46 ] -> 
java.lang.UnsupportedOperationException: 模擬拋錯異常
    at net.sz.TestMain$1$1.run(TestMain.java:93)
    at net.sz.ThreadTest.run(TestMain.java:42)
    at java.lang.Thread.run(Thread.java:745)
[01-20 15:49:11:0875:ERROR: sz.TestMain.run():46 ] -> 
java.lang.UnsupportedOperationException: 模擬拋錯異常
    at net.sz.TestMain$1$1.run(TestMain.java:93)
    at net.sz.ThreadTest.run(TestMain.java:42)
    at java.lang.Thread.run(Thread.java:745)
[01-20 15:49:13:0889:ERROR: sz.TestMain.main():117] -> 執行緒執行耗時:13
[01-20 15:49:14:0890:ERROR: sz.TestMain.main():115] -> 執行緒可能已卡死:1014
[01-20 15:49:15:0876:ERROR: sz.TestMain.run():95 ] -> 1484898555876
[01-20 15:49:15:0876:ERROR: sz.TestMain.run():95 ] -> 1484898555876

我們可以看出,當執行緒執行的時候丟擲異常

其實這種異常,也不存在我那天,對不對,我們能try catch ,

並且可能童鞋都知道,java裡面catch 的程式碼塊如果沒有異常出現,是不會有效能消耗的,如果

丟擲了異常,會建立 exception 物件,並且收集異常資訊,比如呼叫堆疊,行號,檔名,路勁等等;

所以寫程式碼的時候可以加入try catch 語句,放心一般不會影響你的效能;

來來接著

看到這裡我們的執行緒也沒有出問題啊,而且,很安全啊

接下來我先不說情況,給各位模擬一下執行緒出問題的情況

也是一直困擾我很久的事情,曾經遊戲伺服器上線後一度出現執行緒卡死,很長時間,一直懷疑資料庫卡著不動!

 1 public class TestMain {
 2 
 3     private static final Logger log = Logger.getLogger(TestMain.class);
 4     static ThreadTest threadTest = new ThreadTest();
 5     static Thread thread;
 6     static Thread thread1;
 7 
 8     public static void main(String[] args) throws InterruptedException {
 9         /*建立執行緒任務佇列*/
10 
11         thread = new Thread(threadTest);
12         /*啟動執行緒*/
13         thread.start();
14         long i = 0;
15 
16         Random random = new Random();
17 
18         thread1 = new Thread(new Runnable() {
19             @Override
20             public void run() {
21                 while (true) {
22                     try {
23                         /*相當於沒 2秒 有一個任務需要處理*/
24                         Thread.sleep(2000);
25                     } catch (Exception e) {
26                     }
27                     /*建立任務*/
28                     threadTest.runs.add(new Runnable() {
29                         @Override
30                         public void run() {
31                             new TestRun();
32                             log.error(System.currentTimeMillis());
33                         }
34                     });
35                     /*通知執行緒有新任務了*/
36                     synchronized (threadTest.runs) {
37                         threadTest.runs.notify();
38                     }
39                 }
40             }
41         });
42         thread1.start();
43         while (true) {
44             try {
45                 /*相當於沒 1秒 檢查*/
46                 Thread.sleep(1000);
47             } catch (Exception e) {
48             }
49             long timer = System.currentTimeMillis() - threadTest.lastTimer;
50             if (threadTest.lastRun != null) {
51                 if (timer > 500) {
52                     showStackTrace();
53                 } else {
54                     log.error("執行緒執行耗時:" + timer + " " + threadTest.lastRun.getClass().getName());
55                 }
56             }
57         }
58     }
59 
60     /**
61      *
62      * 檢視執行緒堆疊
63      */
64     public static void showStackTrace() {
65         StringBuilder buf = new StringBuilder();
66         /*如果現場意外終止*/
67         long procc = System.currentTimeMillis() - threadTest.lastTimer;
68         if (procc > 5 * 1000 && procc < 864000000L) {//小於10天//因為多執行緒操作時間可能不準確
69             buf.append("執行緒[")
70                     .append(thread.getName())
71                     .append("]")
72                     .append("可能已卡死 -> ")
73                     .append(procc / 1000f)
74                     .append("s\n    ")
75                     .append("執行任務:")
76                     .append(threadTest.lastRun.getClass().getName());
77             try {
78                 StackTraceElement[] elements = thread.getStackTrace();
79                 for (int i = 0; i < elements.length; i++) {
80                     buf.append("\n    ")
81                             .append(elements[i].getClassName())
82                             .append(".")
83                             .append(elements[i].getMethodName())
84                             .append("(").append(elements[i].getFileName())
85                             .append(";")
86                             .append(elements[i].getLineNumber()).append(")");
87                 }
88             } catch (Exception e) {
89                 buf.append(e);
90             }
91             buf.append("\n++++++++++++++++++++++++++++++++++");
92             String toString = buf.toString();
93             log.error(toString);
94         }
95     }
96 }

同樣先改造一下任務模擬執行緒;

這次我們直接new 一個物件;

並且我們增加如果判斷執行緒卡住的話列印執行緒堆疊情況

 1 [01-20 16:09:39:0787:ERROR: sz.TestMain.showStackTrace():149] -> 執行緒[Thread-0]可能已卡死 -> 8.008s
 2     執行任務:net.sz.TestMain$1$1
 3 ++++++++++++++++++++++++++++++++++
 4 [01-20 16:09:40:0787:ERROR: sz.TestMain.showStackTrace():149] -> 執行緒[Thread-0]可能已卡死 -> 9.008s
 5     執行任務:net.sz.TestMain$1$1
 6 ++++++++++++++++++++++++++++++++++
 7 [01-20 16:09:41:0787:ERROR: sz.TestMain.showStackTrace():149] -> 執行緒[Thread-0]可能已卡死 -> 10.008s
 8     執行任務:net.sz.TestMain$1$1
 9 ++++++++++++++++++++++++++++++++++
10 [01-20 16:09:42:0790:ERROR: sz.TestMain.showStackTrace():149] -> 執行緒[Thread-0]可能已卡死 -> 11.011s
11     執行任務:net.sz.TestMain$1$1
12 ++++++++++++++++++++++++++++++++++
13 [01-20 16:09:43:0791:ERROR: sz.TestMain.showStackTrace():149] -> 執行緒[Thread-0]可能已卡死 -> 12.012s
14     執行任務:net.sz.TestMain$1$1
15 ++++++++++++++++++++++++++++++++++
16 [01-20 16:09:44:0791:ERROR: sz.TestMain.showStackTrace():149] -> 執行緒[Thread-0]可能已卡死 -> 13.012s
17     執行任務:net.sz.TestMain$1$1
18 ++++++++++++++++++++++++++++++++++
19 [01-20 16:09:45:0792:ERROR: sz.TestMain.showStackTrace():149] -> 執行緒[Thread-0]可能已卡死 -> 14.013s
20     執行任務:net.sz.TestMain$1$1
21 ++++++++++++++++++++++++++++++++++

一般當玩家反映我們遊戲出問題,以後ssh登入伺服器檢視日誌情況的時候滿篇基本都是這樣的情況;

看上去是執行緒執行某個任務的時候出現了卡住,當時線上遊戲出現的是執行資料庫獲取玩家資料載入,然後卡住了,

當時一度懷疑,我們mysql資料庫不穩定,是不是資料庫問題,但是DBA很明確的告訴我們資料庫沒問題,連線量也很正常;服務很正常;

但是沒辦法,只能現在關閉伺服器;

我們通常使用https的gm命令停止伺服器,可是此時發現,使用http是的gm命令停止伺服器依然沒有任何效果;;

當時已經瓜了,只能痛 kill 命令停止java程序,導致玩家回檔,有時間回檔幾分鐘,有時候回檔幾個小時;

玩家一度下滑;

那個時候非常的煎熬,因為不知道問題到底在哪裡,看程式碼無法反應出來;

於是,去排除最近新新增的所有程式碼;看上去貌似有一個些不合理的程式碼(有新手童鞋),改掉;

提交,編譯,上傳到伺服器,啟動遊戲;能夠正常執行;

一直都是這樣懷疑伺服器邏輯問題;

錯誤日誌

然後公司運維,在查詢伺服器日誌異常列印情況是給我反饋了一部分異常日誌;

我這裡展示我測試程式碼的異常日誌

 1 Exception in thread "Thread-0" java.lang.ExceptionInInitializerError
 2     at net.sz.TestMain$1$1.run(TestMain.java:87)
 3     at net.sz.ThreadTest.run(TestMain.java:47)
 4     at java.lang.Thread.run(Thread.java:745)
 5 Caused by: java.lang.UnsupportedOperationException
 6     at net.sz.TestRun.<clinit>(TestRun.java:18)
 7     ... 3 more
 8 [01-20 16:09:36:0782:ERROR: sz.TestMain.showStackTrace():149] -> 執行緒[Thread-0]可能已卡死 -> 5.001s
 9     執行任務:net.sz.TestMain$1$1
10 ++++++++++++++++++++++++++++++++++

看上去是有錯誤列印的,然後緊接著就出現了執行緒卡死情況;很奇怪明明有try catch 啊

現在我們再來看看,我們的TestRun 這個類的寫法

 1 package net.sz;
 2 
 3 import org.apache.log4j.Logger;
 4 
 5 /**
 6  *
 7  * <br>
 8  * author 失足程式設計師<br>
 9  * mail [email protected]<br>
10  * phone 13882122019<br>
11  */
12 public class TestRun {
13 
14     private static final Logger log = Logger.getLogger(TestRun.class);
15 
16     static {
17         if (true) {
18             throw new UnsupportedOperationException();
19         }
20     }
21 }

由於我是在模擬測試程式碼,

所以直接在loader這個class的時候就init錯誤,丟擲exception,然後一直以為這樣的exception是可以try catch 的,原諒我這麼多年的小白鼠

PS:(當然,當時我遊戲伺服器出現的情況是hibernate和c3p0獲取socket 資料連線導致的;)

直到這兩天我在開發過程中發現這樣的異常情況,導致執行緒卡死,我就開始懷疑了,

當時一個偶發情況想到先去檢視執行緒狀態;

於是在檢視執行緒堆疊裡面加入執行緒當前狀態;

 1     /**
 2      *
 3      * 檢視執行緒堆疊
 4      */
 5     public static void showStackTrace() {
 6         StringBuilder buf = new StringBuilder();
 7         /*如果現場意外終止*/
 8         long procc = System.currentTimeMillis() - threadTest.lastTimer;
 9         if (procc > 5 * 1000 && procc < 864000000L) {//小於10天//因為多執行緒操作時間可能不準確
10             buf.append("執行緒[")
11                     .append(thread.getName())
12                     .append("]")
13                     .append("]當前狀態->")
14                     .append(thread.getState())
15                     .append("可能已卡死 -> ")
16                     .append(procc / 1000f)
17                     .append("s\n    ")
18                     .append("執行任務:")
19                     .append(threadTest.lastRun.getClass().getName());
20             try {
21                 StackTraceElement[] elements = thread.getStackTrace();
22                 for (int i = 0; i < elements.length; i++) {
23                     buf.append("\n    ")
24                             .append(elements[i].getClassName())
25                             .append(".")
26                             .append(elements[i].getMethodName())
27                             .append("(").append(elements[i].getFileName())
28                             .append(";")
29                             .append(elements[i].getLineNumber()).append(")");
30                 }
31             } catch (Exception e) {
32                 buf.append(e);
33             }
34             buf.append("\n++++++++++++++++++++++++++++++++++");
35             String toString = buf.toString();
36             log.error(toString);
37         }
38     }
View Code

 

Exception in thread "Thread-0" java.lang.ExceptionInInitializerError
    at net.sz.TestMain$1$1.run(TestMain.java:87)
    at net.sz.ThreadTest.run(TestMain.java:47)
    at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.UnsupportedOperationException
    at net.sz.TestRun.<clinit>(TestRun.java:18)
    ... 3 more
[01-20 16:26:50:0593:ERROR: sz.TestMain.main():110] -> 執行緒執行耗時:1 net.sz.TestMain$1$1
[01-20 16:26:55:0599:ERROR: sz.TestMain.showStackTrace():151] -> 執行緒[Thread-0]]當前狀態->TERMINATED可能已卡死 -> 5.006s
    執行任務:net.sz.TestMain$1$1
++++++++++++++++++++++++++++++++++
[01-20 16:26:56:0600:ERROR: sz.TestMain.showStackTrace():151] -> 執行緒[Thread-0]]當前狀態->TERMINATED可能已卡死 -> 6.008s
    執行任務:net.sz.TestMain$1$1
++++++++++++++++++++++++++++++++++

好傢伙,不看不知道猛一看嚇了我一條;

難怪執行緒卡死啊,執行緒當前狀態是執行緒已經結束退出執行了;

於是百度了一下,執行緒,java執行緒;

看到一篇文章,就是當java程式碼丟擲 jvm異常

1 Exception in thread "Thread-0" java.lang.ExceptionInInitializerError
2     at net.sz.TestMain$1$1.run(TestMain.java:87)
3     at net.sz.ThreadTest.run(TestMain.java:47)
4     at java.lang.Thread.run(Thread.java:745)
5 Caused by: java.lang.UnsupportedOperationException
6     at net.sz.TestRun.<clinit>(TestRun.java:18)
7     ... 3 more

類似這樣的異常的時候,是不會丟擲來而是直接走

執行緒的

1 public interface UncaughtExceptionHandler

而是執行緒的這個介面的程式碼;

如果你對執行緒是屬於執行緒組的,會呼叫執行緒組的

1     public UncaughtExceptionHandler getUncaughtExceptionHandler() {
2         return uncaughtExceptionHandler != null ?
3             uncaughtExceptionHandler : group;
4     }
1 void uncaughtException(Thread t, Throwable e)

如果執行緒實現了這個介面,或者是歸於執行緒組,走執行緒組的這個方法,拋錯異常,並且結束了當前執行緒;

哎,這就是基礎知識不紮實的我;導致了這個問題;

其實我們在做底層程式碼或者框架設計的時候,能避免一些程式碼和異常規範,但是不能保證沒人都能寫的很規範,或者基礎知識很紮實;

起碼我在這個事情之前也不算過關對吧;

執行緒狀態的監聽

百度的時候看到一個文章;

可以監聽執行緒狀態如果執行緒改變了就通知你,

執行緒修改結果,同時監聽執行緒

1 class MyThread extends Thread {
2 
3     Runnable run;
4 
5     public MyThread(Runnable run) {
6         super(run);
7         this.run = run;
8     }
9 }

我們新增一個mythread類;描述當前執行緒

 修改執行緒啟動方式

 1         /*建立執行緒任務佇列*/
 2         thread = new MyThread(threadTest);
 3 
 4         uncaughtExceptionHandler = new Thread.UncaughtExceptionHandler() {
 5             @Override
 6             public void uncaughtException(Thread t, Throwable e) {
 7                 log.error("收到未能正常捕獲的異常", e);
 8                 if (t instanceof MyThread) {
 9                     /*判斷是我們自定義執行緒模型,建立新的執行緒*/
10                     thread = new MyThread(((MyThread) t).run);
11                     thread.setUncaughtExceptionHandler(uncaughtExceptionHandler);
12                     /*啟動執行緒*/
13                     thread.start();
14                 }
15             }
16         };
17 
18         thread.setUncaughtExceptionHandler(uncaughtExceptionHandler);
19         /*啟動執行緒*/
20         thread.start();

並且修改一下模擬新增任務執行緒

 1 thread1 = new Thread(new Runnable() {
 2             @Override
 3             public void run() {
 4                 while (true) {
 5                     try {
 6                         /*相當於沒 2秒 有一個任務需要處理*/
 7                         Thread.sleep(2000);
 8                     } catch (Exception e) {
 9                     }
10                     /*任務佇列一直不能執行*/
11                     if (threadTest.runs.isEmpty()) {
12                         /*建立任務*/
13                         threadTest.runs.add(new Runnable() {
14