ArrayListスレッドは安全ではありません

4130 ワード

一.概説ArrayListについては、よく知られていないと信じています.このクラスは私たちが普段最も接触しているリストの集合クラスです.
面接の時、面接官はまずその知識について聞くと信じています.よく聞かれる質問の一つは、ArrayListはスレッドが安全かどうかということです.
答えはもちろん簡単で、暗記しても自分でソースコードを見たことがあっても、スレッドが安全ではないことを知っています.では、なぜスレッドが安全ではないのでしょうか.スレッドが安全ではない具体的な体現はどのようなものですか?ソースコードの観点から見てみましょう.
二.ソース分析はまず、このクラスが持つ属性フィールドの一部を見てみましょう.
public class ArrayList extends AbstractList
        implements List, RandomAccess, Cloneable, java.io.Serializable
{
    /**
     *         
     *     ArrayList         ,    EMPTY_ELEMENTDATA   elementData,
     *           ,        DEFAULT_CAPACITY 
     */
    transient Object[] elementData; 

    /**
     *     ,elementData        
     */
    private int size;
}

したがって、この2つのフィールドから、ArrayListの実装は主にObjectの配列を使用して、すべての要素を保存し、現在の配列に追加された要素の数を保存するためにsize変数を使用していることがわかります.
次に、最も重要なadd操作時のソースコードを見てみましょう.
public boolean add(E e) {

    /**
     *        ,        
     * 1.     capacity      ,      
     * 2.                
     */
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

EnsureCapacityInternal()このメソッドの詳細コードはしばらく見ないでください.現在の新しい要素をリストの後ろに追加すると、リストのelementData配列の大きさが満たされるかどうかを判断し、size+1のこの需要長がelementDataという配列の長さより大きい場合は、この配列を拡張します.
これによりadd要素が表示されると、実際には2つの大きなステップが行われます.
elementData配列容量が需要を満たしているかどうかを判断してelementData対応位置に値を設定すると、スレッドが安全でないという最初のリスクが発生し、複数のスレッドがadd操作を行うとelementData配列が境界を越える可能性があります.具体的なロジックは次のとおりです.
リストサイズが9、すなわちsize=9スレッドAがaddメソッドに入り始めると、sizeの値が9を取得し、ensureCapacityInternalメソッドを呼び出して容量判断を行う.スレッドBはaddメソッドにも入り、sizeの値も9となり、ensureCapacityInternalメソッドの呼び出しも開始する.スレッドAは需要サイズが10であることを発見し,elementDataのサイズは10であり収容できる.そこで拡張せず、戻ります.スレッドBも需要サイズが10であることを発見し,収容し,返すことができる.スレッドAは設定値操作を開始し,elementData[size+]=e操作となる.このときsizeは10になります.スレッドBはまた、elementData[10]=eを設定しようとする設定値操作を開始するが、elementDataは拡張されておらず、その下付き文字は最大9である.これにより、配列の境界を越えた異常が報告されます.また、2ステップ目のelementData[size+]=e設定値の操作もスレッドが安全ではありません.ここから,このステップ操作も原子操作ではなく,以下の2つのステップ操作から構成されていることがわかる.
elementData[size] = e; size = size + 1; 単一スレッドでこの2つのコードを実行しても問題はありませんが、マルチスレッド環境で実行すると、あるスレッドの値が別のスレッドに追加された値を上書きする可能性があります.具体的な論理は次のとおりです.
リストサイズは0、つまりsize=0スレッドAが要素の追加を開始し、値はAです.このとき、最初の操作を実行し、elementDataの下に0と表示された位置にAを置きます.次に、スレッドBもちょうどBの値を持つ要素の追加を開始し、最初のステップに進みます.このとき、スレッドBはsizeの値が0のままであることを取得し、elementDataの下に0と表示された位置にBを置く.スレッドAがsizeの値を1スレッドBに増加し始めるsizeの値を2に増加するようにスレッドABの実行が完了すると、理想的にはsizeが2、elementDataの下に0を示す位置がA、下に1を示す位置がBとなる.実際にはsizeが2、elementDataが0の位置がBとなり、1の位置には何もありません.さらに、sizeが2であるため、setメソッドを使用してこの位置の値を変更しない限りnullになります.要素を追加すると、下に2と表示されている位置から始まります.
次に、小さな例で検証します.
三.実例再現私たちは以下のコードで安全性の検証を行うことができる.
public static void main(String[] args) throws InterruptedException {
    final List list = new ArrayList();

    //   A 0-1000   list
    new Thread(new Runnable() {
        public void run() {
            for (int i = 0; i < 1000 ; i++) {
                list.add(i);

                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }).start();

    //   B 1000-2000     
    new Thread(new Runnable() {
        public void run() {
            for (int i = 1000; i < 2000 ; i++) {
                list.add(i);

                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }).start();

    Thread.sleep(1000);

    //       
    for (int i = 0; i < list.size(); i++) {
        System.out.println(" " + (i + 1) + "    :" + list.get(i));
    }
}

最後の出力結果には、次の部分があります.
7番目の要素は:3番目の8番目の要素は:1003番目の9番目の要素は:4番目の要素は:1004番目の11番目の要素は:null 12番目の要素は:1005番目の13番目の要素は:6番目の11番目の要素の値がnullであることがわかります.これは私たちが上述した状況です.
何度もテストすると、配列の境界を越えた異常も再現されます.——————————————————本文はCSDNブロガー「Zorrooo」のオリジナル文章で、CC 4.0 BY-SAの著作権契約に従い、原文の出典リンクと本声明を転載してください.テキストリンク:https://blog.csdn.net/u012859681/article/details/78206494