糟了,銀行線上跑了一年的代碼出事故了
介紹
周末在水群的時(shí)候,發(fā)現(xiàn)有個(gè)小伙伴遇到了一個(gè)線上問題
線程池中線程的狀態(tài)只有一個(gè)為RUNNABLE,其他都為WAITING,問有可能是哪些原因造成的?
線程池有25個(gè)線程,只有一個(gè)線程卡在網(wǎng)絡(luò)讀取上面,狀態(tài)為RUNNABLE,其他線程都為WAITING。
可能有小伙伴們沒用過這個(gè)工具,簡(jiǎn)單介紹一下這個(gè)性能監(jiān)測(cè)工具JMC,JMC是源自JRockit JVM的一套監(jiān)控和管理工具,Oracle在發(fā)布JAVA 7u4(Java 7 Update 40)時(shí)將其包含在JDK中,用戶不再需要單獨(dú)下載
只需要在命令中執(zhí)行jmc即可
應(yīng)用啟動(dòng)配置如下參數(shù)
- -Dcom.sun.management.jmxremote.port=7091
- -Dcom.sun.management.jmxremote.authenticate=false
- -Dcom.sun.management.jmxremote.ssl=false
連接到配置的JMC就能看到各種監(jiān)測(cè)指標(biāo)。
本來我想讓這個(gè)小伙伴把代碼發(fā)過來看看的,可他卻說自己做的是銀行的項(xiàng)目,連不上外網(wǎng),只能用手機(jī)開視頻對(duì)著電腦讓我看個(gè)大概。我復(fù)原一下這個(gè)代碼的場(chǎng)景,估計(jì)很多小伙伴一下就能發(fā)現(xiàn)問題了,因?yàn)槲野讯嘤嗟拇a都省略了,只留了會(huì)造成問題的代碼
- 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;
- }
- @SneakyThrows
- @Override
- public void run() {
- // 建立一個(gè)Socket連接發(fā)送數(shù)據(jù)
- Socket socket = new Socket("127.0.0.1",10006);
- // ...
- // 執(zhí)行最后調(diào)用如下方法
- latch.countDown();
- }
- }
- // 真實(shí)的代碼這里的過程為,每次往線程池里面放一批任務(wù),這一批任務(wù)執(zhí)行完畢,再放下一批任務(wù)
- // 即循環(huán)調(diào)用如下方法
- @SneakyThrows
- public void runTask(List<Task> taskList) {
- CountDownLatch latch = new CountDownLatch(5);
- taskList.forEach(item -> {
- item.setLatch(latch);
- service.submit(item);
- });
- latch.await();
- }
- }
提示一下WAITING狀態(tài)的線程阻塞在LockSupport.park()方法上(用了上圖的JMC工具)
寫個(gè)小插曲,這個(gè)小伙伴一直和我強(qiáng)調(diào)這個(gè)代碼已經(jīng)在線上跑了一年了,一直沒發(fā)生問題。怎么到自己這就發(fā)生問題了,所以他的解決方案是一直看自己修改了哪些部分,但是始終沒看出來問題。
而我的思路就和他不一樣了,因?yàn)橛行゜ug只有在特定場(chǎng)景下才會(huì)出現(xiàn),不要堅(jiān)信之前的代碼就沒有問題,要從問題本身著手
Java線程狀態(tài)
在發(fā)現(xiàn)問題的時(shí)候基礎(chǔ)知識(shí)還是很重要的,回顧一下
簡(jiǎn)易的線程狀態(tài)如下圖
Java Thread線程內(nèi)部有一個(gè)枚舉內(nèi)部類State,定義了Java語言線程狀態(tài)的枚舉值
- NEW(初始化狀態(tài))
- RUNNABLE (可運(yùn)行/運(yùn)行狀態(tài))
- BLOCKED(阻塞狀態(tài))
- WAITING (無時(shí)限等待)
- TIMED_WAITING(有時(shí)限等待)
- TERMINATED(終止?fàn)顟B(tài))
Java將操作系統(tǒng)層面的阻塞狀態(tài)細(xì)分為BLOCK,WAITING,TIMED_WAITING三種狀態(tài)
NEW:新建狀態(tài),線程被創(chuàng)建但未啟動(dòng)的狀態(tài)。創(chuàng)建線程有三種方式
- 繼承Thread類
- 實(shí)現(xiàn)Runnable接口
- 實(shí)現(xiàn)Callable接口
我們最常用的是通過實(shí)現(xiàn)接口這種方式,Runnable和Callable接口的區(qū)別如下
- Runnable無法獲取返回值,而Callable可以獲取返回值
- Runnable無法拋出異常,而Callable可以拋出異常
RUNNABLE(就緒狀態(tài)):調(diào)用start之后運(yùn)行之前的狀態(tài)RUNNING(運(yùn)行狀態(tài)):線程正在運(yùn)行BLOCKED(阻塞狀態(tài)):進(jìn)入以下狀態(tài),有以下幾種情況
- BLOCK(同步阻塞):鎖被其他線程占用,如等待進(jìn)入synchronized方法或者代碼塊
- WAITING(主動(dòng)阻塞):執(zhí)行Object.wait(),Thread.join()等
- TIMED_WAITING(等待阻塞):執(zhí)行Object.wait(long),Thread.sleep(long)等
DEAD(終止?fàn)顟B(tài)):線程執(zhí)行完畢 最后將各種方法補(bǔ)充到線程狀態(tài)圖上
場(chǎng)景還原
造成線程WAITING,一般是調(diào)用了如下3種方法之一
- Object.wait()
- Thread.join()
- LockSupport.park()
排查問題的過程如下
- 在明確了代碼中沒有調(diào)用Object.wait()和Thread.join()后,那基本就確定了是調(diào)用了java.util.concurrent包下面的工具類導(dǎo)致的線程阻塞,因?yàn)閖ava.util.concurrent包下的工具類頻繁使用了LockSupport.park()
- 接著就可以確定是使用CountDownLatch造成的問題了,其他的線程已經(jīng)結(jié)束了,只有一個(gè)線程在運(yùn)行,此時(shí)其他線程就阻塞等待
- 那這個(gè)RUNNABLE的線程做啥了,為啥一直沒有結(jié)束?此時(shí)文章最開始的一張圖指明了方向,這個(gè)線程阻塞在網(wǎng)絡(luò)讀取上了。
- 既然卡在網(wǎng)絡(luò)讀取上,肯定就是沒有設(shè)置連接的超時(shí)時(shí)間,或者讀取的超時(shí)時(shí)間。一問,果然和我想的一樣,沒有設(shè)置
設(shè)置完后,他在本地跑了一下,剛開始還正常運(yùn)行,后來就直接拋出異常了
SocketTimeoutException: connect timed out(連接服務(wù)端超時(shí)) SocketException: Connection reset(服務(wù)端關(guān)閉了連接,但是客戶端還在從連接中讀取數(shù)據(jù))
那為什么剛開始程序能正常跑?后面就開始報(bào)這種連接異常了呢?
- 服務(wù)端確實(shí)并發(fā)太大了
- 服務(wù)端的網(wǎng)路請(qǐng)求用BIO實(shí)現(xiàn)的,一個(gè)請(qǐng)求創(chuàng)建一個(gè)線程,本身就支持不了高并發(fā)
至于是哪種原因?我讓小伙伴找服務(wù)端的開發(fā)人員確認(rèn)了 一下,服務(wù)端居然是使用BIO實(shí)現(xiàn)的。網(wǎng)絡(luò)請(qǐng)求居然不用Netty,還是你們?nèi)涡?
期待我后續(xù)的Netty文章哈,這種事情堅(jiān)決不能再發(fā)生。
本文轉(zhuǎn)載自微信公眾號(hào)「 Java識(shí)堂」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系 Java識(shí)堂公眾號(hào)。