首頁 >Java >Java基礎 >解決一次i++引發的bug

解決一次i++引發的bug

coldplay.xixi
coldplay.xixi轉載
2020-10-19 17:40:411778瀏覽

java基礎教學專欄為大家介紹i 引發的bug。

解決一次i++引發的bug

大家好,身為日常寫bug修bug的我,今天帶給大家前幾天剛修復的一個意外。不得不承認,有我的地方總是會有這麼多bug。

解決一次i++引發的bug

起因

故事的開始發生在前幾天,有一個不是很常用的匯出功能,被使用者回饋出,不管條件是怎麼樣,導出的數據只有一條,但是實際上根據條件查詢是有很多數據,而且頁面中也查詢出很多數據。 (這個問題已經被修復了,所以當時的Kibana日誌也找不到了)於是放下手上的工作,投入其中來看這個問題。

分析

從問題的描述來分析,那麼只可能出現在以下情況:

  1. 根據搜尋條件查詢出來的記錄只有一條。
  2. 對查詢出來的資料進行相關業務處理,導致最後的結果只有一條。
  3. 檔案匯出元件的邏輯處理之後,導致結果只有一條。

題外話
寫到了這裡,突然想到了一個經典面試題,MQ訊息遺失的場景原因分析。哈哈哈,其實大致上也是這麼幾個角度分析。 (有機會寫MQ的文章)
題外話

解決一次i++引發的bug

#於是就一個個來分析:

  1. 找到相關業務的SQL以及對應的參數,查詢可得,資料不只1條,所以第一種情況可以排除。
  2. 中間業務當中有涉及到相關權限、資料敏感等,將這些都放開之後,還是只有1個資料。
  3. 檔案匯出元件在接​​收到資料的時候,列印的日誌也顯示只有一條,那麼可以說明肯定中間相關業務的邏輯發生了問題。

因為這段程式碼都是寫在一整個方法裡面,導致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集合轉換成一個字串二維陣列。但是輸出結果顯示:解決一次i++引發的bug這就符合我們說的,查詢出來有多條,但是輸出只有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)當中的一些資料結構,如圖:

解決一次i++引發的bug

我們可以簡單的理解為,操作數棧的作用是存放資料並且在堆疊中進行計算數據,而局部變數表則是存放變量的一些資訊。
然後我們來看看上面的指令:
0: iconst_3 (先將常數3壓入堆疊)

解決一次i++引發的bug

1: istore_1 (出栈操作,将值赋给第一个参数,也就是将3赋值给index)

解決一次i++引發的bug

2: iload_1  (将第一个参数的值压入栈,也就是将3入栈,此时栈顶的值为3)

解決一次i++引發的bug

3: iinc 1, 1 (将第一个参数的值进行自增操作,那么此时index的值是4)

解決一次i++引發的bug

6: istore_1 (出栈操作,将值赋给第一个参数,也就是将3赋值给index)

解決一次i++引發的bug

也就是说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,只需要进行自增不赋值就可以修复了。

解決一次i++引發的bug

最後

感謝各位能夠看到這裡,以上就是我處理這個bug的全部過程。雖然這只是一個小bug,但這一個小小的bug還是值得學習和思考的。今後還會繼續分享我所發現的bug以及知識點,如果我的文章對你有幫助,也希望各位大佬 點個關注\color{red}{點個關注}##按個讚

################# ##########,再次感謝大家的支持! ###############相關免費學習推薦:#########java基礎教學############

以上是解決一次i++引發的bug的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.im。如有侵權,請聯絡admin@php.cn刪除