糟了,銀行線上跑了一年的代碼出問題了

糟了,銀行線上跑了一年的代碼出問題了

介紹

週末在水群的時候,發現有個小夥伴遇到了一個線上問題

線程池中線程的狀態只有一個為RUNNABLE,其他都為WAITING,問有可能是哪些原因造成的?

糟了,銀行線上跑了一年的代碼出問題了


線程池有25個線程,只有一個線程卡在網絡讀取上面,狀態為RUNNABLE,其他線程都為WAITING。

糟了,銀行線上跑了一年的代碼出問題了


本來我想讓這個小夥伴把代碼發過來看看的,可他卻說自己做的是銀行的項目,連不上外網,只能用手機開視頻對著電腦讓我看個大概。我復原一下這個代碼的場景,估計很多小夥伴一下就能發現問題了,因為我把多餘的代碼都省略了,只留了會造成問題的代碼

<code>public class BankDemo {

    public ExecutorService service = Executors.newFixedThreadPool(5);

    public static class Task implements Runnable {

        private CountDownLatch latch;

        public void setLatch(CountDownLatch latch) {
            this.latch = latch;
        }

        @Override
        public void run() {
            // 建立一個Socket連接發送數據
            Socket socket = new Socket("127.0.0.1",10006);
            // ...
            // 執行最後調用如下方法
            latch.countDown();
        }
    }

    // 真實的代碼這裡的過程為,每次往線程池裡面放一批任務,這一批任務執行完畢,再放下一批任務
    // 即循環調用如下方法
    public void runTask(List taskList) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(5);
        taskList.forEach(item -> {
            item.setLatch(latch);
            service.submit(item);
        });
        latch.await();
    }
}
123456789101112131415161718192021222324252627282930313233/<code>

提示一下WAITING狀態的線程阻塞在LockSupport.park()方法上

寫個小插曲,這個小夥伴一直和我強調這個代碼已經在線上跑了一年了,一直沒發生問題。怎麼到自己這就發生問題了,所以他的解決方案是一直看自己修改了哪些部分,但是始終沒看出來問題。

而我的思路就和他不一樣了,因為有些bug只有在特定場景下才會出現,不要堅信之前的代碼就沒有問題,要從問題本身著手

Java線程狀態

在發現問題的時候基礎知識還是很重要的,回顧一下

簡易的線程狀態如下圖

糟了,銀行線上跑了一年的代碼出問題了


Java Thread線程內部有一個枚舉內部類State,定義了Java語言線程狀態的枚舉值

  1. NEW(初始化狀態)
  2. RUNNABLE (可運行/運行狀態)
  3. BLOCKED(阻塞狀態)
  4. WAITING (無時限等待)
  5. TIMED_WAITING(有時限等待)
  6. TERMINATED(終止狀態)

Java將操作系統層面的阻塞狀態細分為BLOCK,WAITING,TIMED_WAITING三種狀態

NEW:新建狀態,線程被創建但未啟動的狀態。創建線程有三種方式

  1. 繼承Thread類
  2. 實現Runnable接口
  3. 實現Callable接口

我們最常用的是通過實現接口這種方式,Runnable和Callable接口的區別如下

  1. Runnable無法獲取返回值,而Callable可以獲取返回值
  2. Runnable無法拋出異常,而Callable可以拋出異常

RUNNABLE(就緒狀態):調用start之後運行之前的狀態
RUNNING(運行狀態):線程正在運行
BLOCKED(阻塞狀態):進入以下狀態,有以下幾種情況

  1. BLOCK(同步阻塞):鎖被其他線程佔用,如等待進入synchronized方法或者代碼塊
  2. WAITING(主動阻塞):執行Object.wait(),Thread.join()等
  3. TIMED_WAITING(等待阻塞):執行Object.wait(long),Thread.sleep(long)等

DEAD(終止狀態):線程執行完畢
最後將各種方法補充到線程狀態圖上

糟了,銀行線上跑了一年的代碼出問題了

場景還原

造成線程WAITING,一般是調用瞭如下3種方法之一

  1. Object.wait()
  2. Thread.join()
  3. LockSupport.park()

排查問題的過程如下

  1. 在明確了代碼中沒有調用Object.wait()和Thread.join()後,那基本就確定了是調用了java.util.concurrent包下面的工具類導致的線程阻塞,因為java.util.concurrent包下的工具類頻繁使用了LockSupport.park()

  2. 接著就可以確定是使用CountDownLatch造成的問題了,其他的線程已經結束了,只有一個線程在運行,此時其他線程就阻塞等待

  3. 那這個RUNNABLE的線程做啥了,為啥一直沒有結束?此時文章最開始的一張圖指明瞭方向,這個線程阻塞在網絡讀取上了。

  4. 既然卡在網絡讀取上,肯定就是沒有設置連接的超時時間,或者讀取的超時時間。一問,果然和我想的一樣,沒有設置

設置完後,他在本地跑了一下,剛開始還正常運行,後來就直接拋出異常了

糟了,銀行線上跑了一年的代碼出問題了


SocketTimeoutException: connect timed out(連接服務端超時)
SocketException: Connection reset(服務端關閉了連接,但是客戶端還在從連接中讀取數據)

那為什麼剛開始程序能正常跑?後面就開始報這種連接異常了呢?

  1. 服務端確實併發太大了
  2. 服務端的網路請求用BIO實現的,一個請求創建一個線程,本身就支持不了高併發
糟了,銀行線上跑了一年的代碼出問題了


至於是哪種原因?小夥伴找服務端的開發人員確認一下就知道了。不過我猜很大概率是第二種原因,因為我看他們客戶端就是用BIO寫的,網絡請求居然不用Netty,還是你們任性!

期待我後續的Netty文章哈,這種事情堅決不能再發生。


分享到:


相關文章: