由于基本數據類型使用姿勢不對導致的線上"死循環"問題排查

本文要講的是本周我在線上排查的一個"死循環"問題,由于前人的疏忽,導致線上在某一時間段內瘋狂調用第三方服務,并沒有在預期時間內結束。

線上代碼為采用多線程校驗批量任務,如下所示為模擬場景,很容易可以看出,該段代碼的邏輯如下:

  1. 首先采用一個大小為100的線程安全隊列來模擬待校驗任務
  2. 然后起10個線程,每個線程都有10s時間不斷的取隊列中的任務進行校驗
  3. 每次校驗都先從隊列中取一個任務出來,校驗不成功就把任務放回待校驗隊列,且每次校驗完后會休息0.1s
  4. 編號為44的校驗任務是一個永遠都過不去的坎
     public void multiThreadCheckTest(){
        //初始化待校驗隊列,大小為100
        ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();
        for (int i = 0; i < 100; i++) {
            queue.add(i);
        }

        int threads = 10; //起10個線程來做校驗任務
        float maxCheckMillis = 10 * 1000;// 單個線程最長校驗時間為10s
        try{
            final CountDownLatch latch = new CountDownLatch(threads);
            float startCheckTime = System.currentTimeMillis();
            for (int i = 0; i < threads; i++) {
                new Thread(() -> {
                    try {
                        // 每個線程循環執行校驗
                        while (!queue.isEmpty() && System.currentTimeMillis() - startCheckTime < maxCheckMillis) {
                            Integer ii = queue.poll();

                            // 模擬線上出現的問題,卡在44這里執行不過去
                            boolean checkResult = (ii == 44 ? false : true);

                            if(!checkResult){
                                queue.add(ii); //校驗不成功則把任務重新加入到隊列中
                            }
                            Thread.sleep(100);//休息0.1s
                        }
                    } catch (Exception e) {
                        //異常處理
                    } finally {
                        latch.countDown();
                    }
                }).start();
            }
            latch.await();
            while (!queue.isEmpty()) {
                Integer iii = queue.poll();
                System.out.println("No." + iii+ " task check failed within " + (System.currentTimeMillis() - startCheckTime) / 1000 + "s");
            }
        }catch (Exception e){
            //異常處理
        }
    }

依上可知,每個線程每秒的檢驗能力為10次,所在在10秒時間10個線程的校驗能力為: 10*10 *10 = 1000, 足以處理100個任務,所以這段代碼的理想情況應該在10s左右的時間結束,可是實際執行情況如下:

image-20181201192159038

竟然用了131s,比預期的10倍還多。

而我遇到線上的實際每個線程有4分鐘時間來校驗任務,且每次校驗完后沒有休息直接下一次校驗!!!把這個4分鐘如果放大10倍,如果有個任務一直校驗不成功的話,相當于40分鐘時間10個線程在不間斷的執行校驗任務(相當于死循環),且這個校驗是調用第三方的接口,不僅會把自己給累死,還給了別人很大的壓力。還好我們接收到了機器報警,及時重啟了應用。

為什么會出現這種情況???

第一眼看到用了多線程,隊列,還有CountDownLatch類,我還以為是哪里用得不當導致代碼中產生了死鎖,但不管是看代碼還是分析線程堆棧,都發現不了問題。

只有另辟溪徑了,除了上面的線程并發問題,接下來只可能是while循環的判斷有問題了。這段代碼有兩個while循環,很明顯每二個while沒問題,所以只有第一個while的問題了:

while (!queue.isEmpty() && System.currentTimeMillis() - startCheckTime < maxCheckMillis)

這個while循環當隊列為空或到執行了指定的時間后就會停止,隊列明顯不會為空,所以只能靠第二個條件了。

既然和線程并發無關,和隊列無關,為了便于分析,對代碼進行簡化如下:

public void multiThreadCheckTest(){
        try {
            float maxCheckMillis = 10 * 1000;
            float startCheckTime = System.currentTimeMillis();
            while (System.currentTimeMillis() - startCheckTime < maxCheckMillis) {
                Thread.sleep(100);//每校驗一次休息0.1s
            }
            System.out.println((System.currentTimeMillis() - startCheckTime) / 1000);
        } catch (Exception e) {
            //異常處理
        }
    }

理想情況是輸出一個接近10左右的值,可實際上輸出的是131.072.

果不其然,出問題的就在這幾行代碼里了。對while循環里的左邊的值進行打印:

public void multiThreadCheckTest(){
        try {
            float maxCheckMillis = 10 * 1000;
            float startCheckTime = System.currentTimeMillis();
            while (System.currentTimeMillis() - startCheckTime < maxCheckMillis) {
                System.out.println(System.currentTimeMillis() - startCheckTime);
                Thread.sleep(100);//每校驗一次休息0.1s
            }
            System.out.println((System.currentTimeMillis() - startCheckTime) / 1000);
        } catch (Exception e) {
            //異常處理
        }
    }

輸出卻和預想中并不一樣:

0.0
0.0
0.0
...
0.0
0.0
0.0
131.072

全部為0.0直到最后用了100多秒時間結束,所以問題只能出現在startCheckTime變量的初始化上了,System.currentTimeMillis()返回的是一個long類型的值,但這里卻利用java基本數據類型的自動轉換用一個float類型的值來接收,我們將float改為long后輸出如下:

0
102
204
306
409
...
9790
9890
9991
10

最后就是在10秒結束,問題解決。

接著又試了一下將float改為double,輸出如下:

0.0
102.0
208.0
311.0
...
9774.0
9879.0
9980.0
10.081

結果也符合預期!!!

看到這里你或許和我開始一樣產生了如下的疑問:

  1. long類型占用8個字節,double只占用4個字節,為什么long類型能轉換為double?

  2. 為什么將startCheckTime變量類型改為long和double都可以,偏偏用float就不行呢?

接下來就帶著這兩個疑問來復習下基礎知識了。

首先看看java中基本數據類型分類和占用的字節數:


image.png

支持的自動類型轉換規則如下:

image-20181201204142952
  • 首先解決第一個疑問。

對于byte, short, int和long四個整數類而言,它們在內存中都是直接換算成二進制存儲的, 下面以byte為例:

占用1字節即8bit,每一位都是二進制的0或1,且第一位為表示正負的符號位,最大為0111 1111,轉換為10進制為127,最小為1111 1111,轉換為十進制為-128(1000 000, 用補碼計算, 負數的袚即原碼非符號位取反加1, -128 = (-1) + (-127) = [1000 0001]原 + [1111 1111]原 = [1111 1111]補 + [1000 0001]補 = [1000 0000]補 ), 所以byte的取值范圍為-128~127。同理

short(-32768~32767),

int(-2147483648~2147483647)

long(-9223372036854774808~9223372036854774807)

而浮點數是以科學計數法的形式存儲,以單精度類型float為例,占用4個字節即32bit,第一個bit為符號位,接下來8bits為指數位(取值班范圍為-128-127),剩下23bits為小數位。所以float的取值范圍為3.402823e+38 ~ 1.401298e-45(e+38表示是乘以10的38次方,e-45表示乘以10的負45次方),同理double雙精度有11個指數位52個小數位,取值范圍為1.797693e+308~ 4.9000000e-324。

由于可知,因為在內存中的存儲形式不同,雖然float類型只占4個字節,但是表示的數值范圍遠遠大于long的數值范圍, 所以long類型能轉換為float類型。

  • 接下來解決第二個問題。

浮點數之所以稱為浮點,是因為它會產生精度丟失。計算機世界只能用二進制的小數來表達小數,而我們現實世界是用十進制的小數來表達。對于二進制小數,小數點右邊能表達的值是 1/2, 1/4, 1/8, 1/16, 1/32, 1/64, 1/128 … 1/(2^n)。所有這些小數都是一點一點的拼湊出來的一個近似的數值, 所有才會不準確的。

舉個例子, 現在用二進制來表示十進制的1.2:
1.01 = 1 + 1/4 = 0.25 , 偏大
1.001 = 1 + 1/8 = 0.125 , 偏小
1.0011 = 1 + 1/8 + 1/16 = 0.1875 ,
1.001101 = 1 + 1/8+ 1/16 + 1/64 = 0.203125 , 又偏大
1.0011001 = 1 + 1/8 + 1/16 + 1/128 = 0.1953125 ,
1.00110011 = 1 + 1/8+1/16+1/128+1/256 = 0.19921875 , 這個很接近
越來越接近…
這就是所謂的用二進制小數沒法精確表達10進制小數的意思。

因為浮點數表示的小數位不同所以精度不同。

float:2^23 = 8388608,一共七位,這意味著最多能有7位有效數字,但絕對能保證的為6位,也即float的精度為6~7位有效數字;

double:2^52 = 4503599627370496,一共16位,同理,double的精度為15~16位。

回到我們要解決的問題,打印出System.currentTimeMillis()的一個值:

1543671830108 = 1.543671830108E+12

轉換為float: 10 ^ 12 / 2^ 23 = 119209 ms = 119s (java中為131072ms = 131s, 有待考究)

轉換為double: 10 ^ 12 / 2^ 52 = 0 可以忽略

通過上面計算把時時間戳轉換為float后會損失精度,有一百多秒的精度損失, 轉換為double后幾乎沒有精度損失,這就是為什么可以將時間戳轉換為double而不能轉換為float了, 當然最好還是直接使用long, 因為long已經足以表達時間戳。

調整后的代碼如下, 不管怎樣都不會再出現執行時間與預期時間不符的"死循環"了:

  1. 將maxCheckMillis, startCheckTime變量類型都改為long
  2. while循環里只有在校驗不成功才暫停0.1s
public void multiThreadCheckTest(){
        //初始化待校驗隊列,大小為100
        ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();
        for (int i = 0; i < 100; i++) {
            queue.add(i);
        }

        int threads = 10; //起10個線程來做校驗任務
        long maxCheckMillis = 10 * 1000;// 單個線程最長校驗時間為10s
        try{
            final CountDownLatch latch = new CountDownLatch(threads);
            long startCheckTime = System.currentTimeMillis();
            for (int i = 0; i < threads; i++) {
                new Thread(() -> {
                    try {
                        // 每個線程循環執行校驗
                        while (!queue.isEmpty() && System.currentTimeMillis() - startCheckTime < maxCheckMillis) {
                            Integer ii = queue.poll();

                            // 模擬線上出現的問題,卡在44這里執行不過去
                            boolean checkResult = (ii == 44 ? false : true);

                            if(!checkResult){
                                //校驗不成功則記錄日志,并休息0.1s, 然后把任務重新加入到隊列中
                                System.out.println(Thread.currentThread() + " check " + ii + " failed!");
                                Thread.sleep(100);//校驗不成功則休息0.1s
                                queue.add(ii);
                            }
                        }
                    } catch (Exception e) {
                        //異常處理
                    } finally {
                        latch.countDown();
                    }
                }).start();
            }
            latch.await();
            while (!queue.isEmpty()) {
                Integer iii = queue.poll();
                System.out.println("No." + iii+ " task check failed within " + (System.currentTimeMillis() - startCheckTime) / 1000 + "s");
            }
        }catch (Exception e){
            //異常處理
        }
    }

總結: 由于疏忽大意使用基本數據類型姿勢不對,while循環內的條件判斷沒有在預期時間內為非,最多多出100多s, 也就是兩分鐘的時間一直在重復執行,讓人誤以為出現的死循環。所以平時寫代碼時還是要嚴于律己,不要給以后或后來人留坑。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容

  • 國家電網公司企業標準(Q/GDW)- 面向對象的用電信息數據交換協議 - 報批稿:20170802 前言: 排版 ...
    庭說閱讀 11,043評論 6 13
  • 觸碰 感情鮮勾勾肩 關系對搭搭背 媽媽 你知道嗎 我有多想你 媽媽 再側點身 這樣 我踮著腳尖 就能摟著你了 媽媽...
    枕槿折槐閱讀 911評論 6 8
  • 只需要這一個簡單的套路,實現公眾號文章閱讀量10萬+ 今天要分享的這個公眾號套路,我相信很多人之前都在朋友圈,或是...
    三碗烈酒與友閱讀 828評論 0 0
  • 什么是創新? 創新是指以現有的思維模式提出有別于常規或常人思路的見解為導向,利用現有的知識和物質,在特定的環境中,...
    EK_962d閱讀 372評論 0 0