欄目為大家介紹i++引發的bug。
大家好,作為日常寫bug修bug的我,今天給大家帶來前幾天剛剛修復的一個事故。不得不承認,有我的地方總是會有這麼多bug。
故事的開始發生在前幾天,有一個不是很常用的匯出功能,被使用者反饋出,不管條件是怎麼樣,匯出的資料只有一條,但是實際上根據條件查詢是有很多資料,而且頁面中也查詢出很多資料。(這個問題已經被修復了,所以當時的Kibana紀錄檔也找不到了)於是放下手上的工作,投入其中來看這個問題。
從問題的描述中來分析,那麼只可能出現在以下情況:
題外話
寫到了這裡,突然想到了一個經典面試題,MQ訊息丟失的場景原因分析。哈哈哈,其實大致上也是這麼幾個角度分析。(有機會來寫MQ的文章)
題外話
於是就一個個來分析:
由於這段程式碼都是寫在一整個方法裡面,導致Arthas排查起來就比較困難,只好一步步設定紀錄檔進行排查。(所以,如果是一大段邏輯,建議是拆分成duoge 子方法,一來在寫的時候思路明確,有一種模組化的概念,至於方法複用什麼的我就不多提了,基本操作;二是一但發生問題,排查起來也會方便點,經驗之談)。
最終定位到一個for迴圈裡面。
話不多說,我們直接來看程式碼。眾所周知,我向來是一個很保護公司程式碼的人,所以,我在這裡又不得不給大家模擬一下了。從問題的情況來看,是匯出的物件記錄是空
import com.google.common.collect.Lists;import java.util.List;public class Test { public static void main(String[] args) { // 獲取Customer資料,這裡就簡單模擬 List<Customer> customerList = Lists.newArrayList(new Customer("Java"), new Customer("Showyool"), new Customer("Soga")); int index = 0; String[][] exportData = new String[customerList.size()][2]; for (Customer customer : customerList) { exportData[index][0] = String.valueOf(index); exportData[index][1] = customer.getName(); index = index++; } System.out.println(JSON.toJSONString(exportData)); } }class Customer { public Customer(String name) { this.name = name; } private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }複製程式碼
這段程式碼看起來好像也沒什麼的,就是將Customer集合轉換成一個字串二維陣列。但是輸出結果顯示:這就符合我們說的,查詢出來有多條,但是輸出只有1條。
仔細觀察一下,我們可以發現,輸出的資料顯示都是最後一條,也就是說,Customer這個集合每次遍歷的時候,都是後者將前者進行覆蓋,也就是說,這個index的下標一直沒有變化過,一直是0。
這樣看來,我們的這個自增確實有點問題,那麼我們再簡單來寫一個模型
public class Test2 { public static void main(String[] args) { int index = 3; index = index++; System.out.println(index); } }複製程式碼
我們將上面的業務邏輯簡化成這樣一個模型,那麼這個結果毫無意外的是3。
那麼我們執行一下javap,看看JVM位元組碼是如何解釋:
javap -c Test2 Compiled from "Test2.java"public class com.showyool.blog_4.Test2 { public com.showyool.blog_4.Test2(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: iconst_3 1: istore_1 2: iload_1 3: iinc 1, 1 6: istore_1 7: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 10: iload_1 11: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 14: return}複製程式碼
這裡我簡單講一下這裡的JVM位元組碼指令(後面有機會再詳細寫寫文章)
首先我們需要先知道這裡存在兩個概念,運算元棧和區域性變數表,這兩者是存在虛擬機器器棧當中棧幀(stack frame)當中的一些資料結構,如圖:
我們可以簡單的理解為,運算元棧的作用是存放資料並且在棧中進行計算資料,而區域性變數表則是存放變數的一些資訊。
然後我們來看看上面的指令:
0: iconst_3 (先將常數3壓入棧)
1: istore_1 (出棧操作,將值賦給第一個引數,也就是將3賦值給index)
2: iload_1 (將第一個引數的值壓入棧,也就是將3入棧,此時棧頂的值為3)
3: iinc 1, 1 (將第一個引數的值進行自增操作,那麼此時index的值是4)
6: istore_1 (出棧操作,將值賦給第一個引數,也就是將3賦值給index)
也就是說index這個引數的值是經歷了index->3->4->3,所以這樣一輪操作之後,index又回到了一開始賦值的值。
這樣一來,我們發現,問題其實出在最後一步,在進行運算之後,又將原先棧中記錄的值重新賦給變數,覆蓋掉了 如果我們這樣寫:
public class Test2 { public static void main(String[] args) { int index = 3; index++; System.out.println(index); } } Compiled from "Test2.java"public class com.showyool.blog_4.Test2 { public com.showyool.blog_4.Test2(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: iconst_3 1: istore_1 2: iinc 1, 1 5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 8: iload_1 9: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 12: return}複製程式碼
可以發現,這裡就沒有最後一步的istore_1,那麼在iinc之後,index的值就變成我們預想的4。
還有一種情況,我們來看看:
public class Test2 { public static void main(String[] args) { int index = 3; index = index + 2; System.out.println(index); } } Compiled from "Test2.java"public class com.showyool.blog_4.Test2 { public com.showyool.blog_4.Test2(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: iconst_3 1: istore_1 2: iload_1 3: iconst_2 4: iadd 5: istore_1 6: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 9: iload_1 10: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 13: return}複製程式碼
0: iconst_3 (先將常數3壓入棧)
1: istore_1 (出棧操作,將值賦給第一個引數,也就是將3賦值給index)
2: iload_1 (將第一個引數的值壓入棧,也就是將3入棧,此時棧頂的值為3)
3: iconst_2 (將常數2壓入棧, 此時棧頂的值為2,2在3之上)
4: iadd (將棧頂的兩個數進行相加,並將結果壓入棧。2+3=5,此時棧頂的值為5)
5: istore_1 (出棧操作,將值賦給第一個引數,也就是將5賦值給index)
看到這裡各位觀眾老爺肯定會有這麼一個疑惑,為什麼這裡的iadd加法操作之後,會影響棧裡面的資料,而先前說的iinc不是在棧裡面操作?好的吧,我們可以看看JVM虛擬機器器規範當中,它是這麼描述的:
指令iinc對給定的區域性變數做自增操作,這條指令是少數幾個執行過程中完全不修改運算元棧的指令。它接收兩個運算元: 第1個區域性變數表的位置,第2個位累加數。比如常見的i++,就會產生這條指令
看到這裡,我們知道,對於一般的加法操作之後複製沒啥問題,但是使用i++之後,那麼此時棧頂的數還是之前的舊值,如果此刻進行賦值就會回到原來的舊值,因為它並沒有修改棧裡面的資料。所以先前那個bug,只需要進行自增不賦值就可以修復了。
感謝各位能夠看到這裡,以上就是我處理這個bug的全部過程。雖然這只是一個小bug,但是這一個小小的bug還是值得學習和思考的。今後還會繼續分享我所發現的bug以及知識點,如果我的文章對你有所幫助,還希望各位大佬,再次感謝大家的支援!
相關免費學習推薦:
以上就是解決一次i++引發的bug的詳細內容,更多請關注TW511.COM其它相關文章!