i++ に起因するバグを解決する

coldplay.xixi
リリース: 2020-10-19 17:40:41
転載
1741 人が閲覧しました

java Basic Tutorial 列では、i によって引き起こされるバグを紹介します。

i++ に起因するバグを解決する

皆さん、こんにちは。毎日バグを書いて修正している者として、今日は数日前に修正されたばかりの事故を紹介します。どこにいても常にバグがたくさんあることを認めざるを得ません。

i++ に起因するバグを解決する

原因

この話は数日前に始まりました。あまり一般的に使用されておらず、ユーザーによって報告されたエクスポート機能がありました。条件は でした。エクスポートされたデータは 1 つだけですが、実際には条件に従って大量のデータがクエリされており、ページ上でも大量のデータがクエリされています。 (この問題は修正されたため、当時の Kibana ログはもう見つかりません。) そこで、私は仕事をやめて、この問題を調査することにしました。

分析

問題の説明によると、この問題は次の状況でのみ発生する可能性があります。

  1. に基づいてクエリされたレコードは 1 つだけです。検索条件
  2. クエリされたデータに対して関連するビジネス処理を実行し、最終結果は 1 つだけになります。
  3. ファイル エクスポート コンポーネントの論理処理後の結果は 1 つだけです。

余談
これを書いた後、MQメッセージ損失の原因の分析という古典的な面接の質問を突然思い出しました。ははは、実はいくつかの角度から大まかに分析されています。 (機会があれば MQ についての記事を書きます)
余談

i++ に起因するバグを解決する

ということで、一つずつ分析していきます:

  1. クエリで取得できる関連業務のSQLと対応するパラメータを検索します データは複数あるため、最初の状況は除外できます。
  2. 中間ビジネスには、関連する権限、データの機密性などが含まれます。これらを解放した後も、データは 1 つだけです。
  3. ファイル エクスポート コンポーネントがデータを受信すると、出力されるログにもエントリが 1 つだけ表示されます。これは、関連するビジネスのロジックに問題があることを意味します。

このコードはメソッド全体に記述されているため、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 コレクションを文字列の 2 次元配列に変換するものです。しかし、出力結果は次のようになります: i++ に起因するバグを解決するこれは私たちが言ったことと一致しています。複数のクエリがありますが、出力されるのは 1 つだけです。
よく見ると、出力データは常に最後のデータであることがわかります。つまり、Customer コレクションが走査されるたびに、後者が前者を上書きします。つまり、このインデックスの下位部分は、スケールは決して変更されず、常に 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 バイトコード命令について簡単に説明します (後で説明を書きます)機会があれば詳しく説明します)
まず、ここにはオペランド スタックとローカル変数テーブルという 2 つの概念があることを知っておく必要があります。これら 2 つは、仮想マシンのスタック フレーム内のデータ構造です。

i++ に起因するバグを解決する

# オペランド スタックの機能はスタックにデータを格納し、データを計算することであり、ローカル変数はtable は変数に何らかの情報を格納するためのものです。
次に、上記の命令を見てみましょう:
0: iconst_3 (最初に定数 3 をスタックにプッシュします)

i++ に起因するバグを解決する

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

i++ に起因するバグを解決する

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

i++ に起因するバグを解決する

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

i++ に起因するバグを解決する

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

i++ に起因するバグを解決する

也就是说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++ に起因するバグを解決する

最後に

ご覧いただきありがとうございます。上記は、このバグに対処する私のプロセス全体です。これは単なる小さなバグですが、この小さなバグにも学習し、検討する価値があります。今後も私が見つけたバグやナレッジ ポイントを共有していきたいと思います。私の記事がお役に立てば、皆さんも幸いです。 クリックしてフォローしてください\color{red}{クリックしてフォロー} いいね!\color{red}{いいね!} 関連する無料学習の推奨事項:

Java 基本チュートリアル

以上がi++ に起因するバグを解決するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:juejin.im
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート