複数のスレッドでArrayListを使用する必要がある場合はどうなりますか?

複数のスレッドでArrayListを使用する必要がある場合はどうなりますか?
こんにちは、私は山を見ています。

JavaのArrayListはスレッドセーフではないことは誰もが知っています。この知識は非常によく知られているため、インタビュー中でもめったに尋ねられません。

しかし、私たちは本当に原理を知っていますか?または、マルチスレッドの状況でArrayListを使用するとどうなるか知っていますか?

少し前にピットを踏んで、直接2つのピットを踏んだので、今日は拾います。

Cuihua、ソースコード

私はについてお話しましょうロジックコードの前にArrayListadd

  1. キュー内の配列が要素に追加されていないかどうかを確認してください
  2. はいの場合は、現在の必要な長さを10に設定し、そうでない場合は、現在の必要な長さを現在のキューの長さ+1に設定します。
  3. 必要な長さがアレイのサイズより大きいかどうかを判別します
  4. そうである場合は、拡張する必要があり、配列の長さは1.5倍に拡張されます(最初の拡張は0から10に直接なり、フォローアップは1.5倍のステップで増加します)
  5. 配列に要素を追加し、キューの長さを+1します

コードが添付されており、興味のある方はソースコードをご覧ください。

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return <tt>true</tt> (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    
    
    // 判断数组容量是否足够,如果不足,增加1.5倍,size是当前队列长度
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 给下标为size的赋值,同时队列长度+1,下标从0开始
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    
    
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    
    
    // 判断是否首次添加元素,如果是,返回默认队列长度,现在是10
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    
    
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    // 如果不是首次添加元素,就返回当前队列长度+1
    return minCapacity;
}

private void ensureExplicitCapacity(int minCapacity) {
    
    
    modCount++;

    // overflow-conscious code
    // 如果需要的长度大于队列中数组长度,扩容,如果可以满足需求,就不用扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

/**
 * Increases the capacity to ensure that it can hold at least the
 * number of elements specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
private void grow(int minCapacity) {
    
    
    // overflow-conscious code
    int oldCapacity = elementData.length;
    // 这里就是扩容1.5倍的代码
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

それはとても安全ではありません

上記のコードからわかるように、ArrayListマルチスレッドを考慮する要素はなく、完全な効率が優先されます。

奇妙なArrayIndexOutOfBoundsException

まず、仮定を立てます。この時点で、配列の長さがクリティカルエッジに達しています。たとえば、現在の容量は10で、現在は9つの要素、つまりsize = 9であり、2つのスレッドが要素を追加します。同時にキューに:

  1. スレッド1はaddメソッドの入力を開始し、size = 9を取得し、ensureCapacityInternalメソッドを呼び出して容量を判断します。この時点で、配列の容量は10であり、拡張は必要ありません。
  2. スレッド2もaddメソッドに入り、size = 9を取得し、ensureCapacityInternalメソッドを呼び出して容量を判断します。この時点では、配列の容量はまだ10であり、拡張する必要はありません。
  3. スレッド1は値の割り当てを開始します。つまりelementData[size++] = e、この時点でサイズは10になり、配列容量の制限に達します。
  4. スレッド2は、size = 10を使用して、今回は割り当て操作を開始します。つまりelementData[10] = e、添え字は0から始まり、現在の配列容量は10であり、配列が範囲外であることを直接報告しますArrayIndexOutOfBoundsException

わずか1歩先で、スレッド2は例外をスローする殺人者になりました。しかし、例外をスローすることは依然として良いことです。何かがうまくいかなかったことがわかっているので、例外に従うことができます。

奇妙なnull要素

この状況をコードから見つけるのは簡単ではなく、コードを少し変更する必要があります。elementData[size++] = eこのコードは実際には2つのステップを実行します。

elementData[size] = e;
size++;

値を割り当てるスレッドがまだ2つあり、この時点で配列の長さが比較的豊富であるとします。たとえば、配列の長さは10で、現在のサイズは5です。

  1. スレッド1はaddメソッドの入力を開始し、size = 5を取得し、ensureCapacityInternalメソッドを呼び出して容量を判断します。この時点で、配列の容量は10であり、拡張は必要ありません。
  2. スレッド2もaddメソッドに入り、size = 5を取得し、ensureCapacityInternalメソッドを呼び出して容量を判断します。この時点では、配列の容量はまだ10であり、拡張する必要はありません。
  3. スレッド1は割り当てと実行を開始します。elementData[size] = eこの時点でsize = 5であり、実行する前にsize++、スレッド2が割り当てを開始します。
  4. スレッド2は割り当てと実行を開始しelementData[size] = eます。この時点では、サイズはまだ5であるため、スレッド2はスレッド1によって割り当てられた値を上書きします。
  5. スレッド1が実行を開始します。size++この時点でsize = 6
  6. スレッド2が実行を開始します。size++この時点でsize = 7

つまり、2つの要素が追加され、キューの長さが+2になりますが、実際には1つの要素のみがキューに追加され、1つが上書きされます。

この場合、エラーはすぐには報告されないため、トラブルシューティングが非常に面倒です。また、JDK 8の普及により、フィルターを使用して空の要素を自由にフィルター処理できるため、エラーはすぐに発生せず、ビジネス例外が発生するまで検出されません。それまでに、エラーサイトは姿を消し、調査は混乱している。

一部の学生は、ソースコードで、それはelementData[size++] = e1行の操作であると尋ねるかもしれませんが、なぜ実行のために2つのステップに分割されるのですか?実際、これはJVMバイトコードから開始する必要があります。

JVMバイトコードを介して2番目のタイプの例外の原因について話します

簡単なコードから始めましょう:

public class Main {
    
    
    public static void main(String[] args) {
    
    
        int[] nums = new int[3];
        int index = 0;
        nums[index++] = 5;
    }
}

javac Main.javaとのjavap -v -l Main.class組み合わせ操作でバイトコード取得します。

以下の中国語は、私が追加したコメントです。コメントには、ローカル変数テーブルとスタック値の変更もリストされていますが、少し忍耐が必要です。

public class Main
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#12         // java/lang/Object."<init>":()V
   #2 = Class              #13            // Main
   #3 = Class              #14            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               main
   #9 = Utf8               ([Ljava/lang/String;)V
  #10 = Utf8               SourceFile
  #11 = Utf8               Main.java
  #12 = NameAndType        #4:#5          // "<init>":()V
  #13 = Utf8               Main
  #14 = Utf8               java/lang/Object
{
  public Main();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1                                                                               局部变量表                             栈
         0: iconst_3                     // 将int型(3)推送至栈顶                                                      args                                3
         1: newarray       int           // 创建一个指定原始类型(如int, float, char…)的数组,并将其引用值压入栈顶          args                                数组引用
         3: astore_1                     // 将栈顶引用型数值存入第二个本地变量                                            args, nums=数组引用                  null
         4: iconst_0                     // 将int型(0)推送至栈顶                                                       args, nums=数组引用                  0
         5: istore_2                     // 将栈顶int型数值存入第三个本地变量                                             args, nums=数组引用, index=0         null
         6: aload_1                      // 将第二个引用类型本地变量推送至栈顶                                             args, nums=数组引用, index=0         数组引用
         7: iload_2                      // 将第三个int型本地变量推送至栈顶                                               args, nums=数组引用, index=0         0, 数组引用
         8: iinc          2, 1           // 将指定int型变量增加指定值(i++, i--, i+=2),也就是第三个本地变量增加1            args, nums=数组引用, index=1         0, 数组引用
        11: iconst_5                     // 将int型(5)推送至栈顶                                                        args, nums=数组引用, index=1         5, 0, 数组引用
        12: iastore                      // 将栈顶int型数值存入指定数组的指定索引位置                                       args, nums=数组引用, index=1         null
        13: return                       // 从当前方法返回void
      LineNumberTable:
        line 3: 0                        // int[] nums = new int[3];
        line 4: 4                        // int index = 0;
        line 5: 6                        // nums[index++] = 5;
        line 6: 13                       // 方法结尾默认的return
}

上記のバイトコードからわかるように、nums[index++] = 5この文は6から12までの5つの命令に変換されます。一般的な操作は次のとおりです。

  1. 配列と添え字をスタックにプッシュします
  2. 下付き文字に値を追加する
  3. 新しい値をスタックにプッシュします
  4. スタックの上位3つの要素を取得し、要素への添え字の割り当てを開始します

つまり、エラーは、配列の割り当て操作中に、配列参照と添え字が同時にスタックの一番上にプッシュされることです。これは、添え字の割り当ての2つのステップです。マルチスレッド環境では、null値上記が存在する可能性があります。

解決

実際、ソリューションも非常に単純です。つまり、マルチスレッド環境を認識し、ArrayListを使用しないということです。あなたが使用することができますCollections.synchronizedList()返さ同期キューを、あなたも使用することができCopyOnWriteArrayList、このキューを、または拡張ArrayListそれをするために自分自身をaddメソッド同期方式にします。

記事の最後の要約

ArrayListクラス全体の操作はスレッドセーフではなく、マルチスレッド環境で使用すると、問題が発生する可能性があります。上記のadd操作には2つの異常な動作があります。1つは配列の範囲外の例外であり、もう1つは数値の損失とnull値の発生です。これはちょうど最も簡単ですadd。操作の場合addaddAllおよびget混合使用は、より多くの異常な状況が存在します。したがって、使用する場合は、シングルスレッド操作であるかどうかに注意する必要があります。そうでない場合は、雷保護のために他のキューを使用してください。


こんにちは、私はKanshanです。パブリックアカウント:Kanshanのコテージ、10年前のバックエンド、Apache Storm、WxJava、Cynomysのオープンソース寄稿者。主な仕事:プログラマー、アルバイト:建築家。コードの世界で泳ぎ、ドラマでの生活を楽しんでください。

個人のホームページ:https//www.howardliu.cn
個人のブログ投稿:複数のスレッドでArrayListを使用する必要がある場合はどうなりますか?
CSDNホームページ:http:
//blog.csdn.net/liuxinghao CSDNブログ投稿:複数のスレッドでArrayListを使用する必要がある場合はどうなりますか?

公開番号:山小屋を見る

おすすめ

転載: blog.csdn.net/conansix/article/details/113666581