同時リストのソースコード分析

同時実行パッケージ内の唯一の同時実行リストは CopyOnWriteArrayList です。

CopyOnWriteArrayList はスレッドセーフな ArrayList であり、変更操作は基になる層のコピーされた配列 (スナップショット) に対して実行されます。つまり、コピー オン ライト戦略が使用されます。

ここに画像の説明を挿入します

CopyOnWriteArrayList のクラス図では、各 CopyOnWriteArrayList オブジェクトには特定の要素を格納するための配列 array オブジェクトがあり、同時に 1 つのスレッドだけが配列を変更することを保証するために ReentrantLock 排他的ロック オブジェクトが使用されます。

ここで、ReentrantLock は排他的ロックであり、同時に取得できるのは 1 つのスレッドだけであることに注意してください (JUC のロックについては後ほど紹介します)。

コピーオンライトのスレッドセーフ リストを作成するように求められたら、どうしますか?どのような点を考慮する必要がありますか?

  • リストはいつ初期化されますか? 初期化されたリスト要素の数は何ですか? リストのサイズには制限がありますか?
  • スレッドの安全性を確保する方法、たとえば、複数のスレッドが読み書きしているときにスレッドの安全性を確保する方法は?
  • イテレータを使用してリストを走査するときにデータの一貫性を確保するにはどうすればよいでしょうか?

主な手法のソースコード解析

初期化

まず、パラメーターなしのコンストラクターを見てみましょう。次のコードは、配列の初期値としてサイズ 0 の Object 配列を内部的に作成します。
ここに画像の説明を挿入します
次に、パラメーター化されたコンストラクターを確認します。

ここに画像の説明を挿入します

要素の追加

CopyOnWriteArrayList に要素を追加するために使用される関数には、add(E e)、add(int index,E element)、addIfAbsent(E e)、および addAllAbsent(Collection<?extends E> c) が含まれます。これらの原理は似ているため、add (E e) 例を挙げて説明しましょう。

ここに画像の説明を挿入します

上記のコードでは、add メソッドを呼び出すスレッドは、まずコード (1) を実行して排他ロックを取得します。複数のスレッドが add メソッドを呼び出した場合、1 つのスレッドだけがロックを取得し、他のスレッドはブロックされるまでブロックされ、一時停止されます。ロックが解除されます。

したがって、スレッドがロックを取得した後は、スレッドが要素を追加している間、他のスレッドが配列を変更しないことが保証されます。

スレッドがロックを取得した後、コード (2) を実行して配列を取得し、次にコード (3) を実行して配列を新しい配列にコピーします (ここから、新しい配列のサイズが元の配列であることがわかります)サイズが 1 増加するため、CopyOnWriteArrayList は無制限のリストになります)、新しい配列は、新しい配列に要素を追加します。

次に、コード (4) を実行して、元の配列を新しい配列に置き換え、戻る前にロックを解除します。

ロックにより、追加プロセス全体がアトミック操作になります。

要素を追加する場合、元の配列に直接ではなく、スナップショットが最初にコピーされ、次にスナップショットに追加されることに注意してください。

指定した位置の要素を取得する

E get(int index) を使用して、添え字がindexである要素を取得します。要素が存在しない場合は、IndexOutOfBoundsException 例外がスローされます。
ここに画像の説明を挿入します
上記のコードでは、 thread ) の場合、これは 2 段階の操作ですが、プロセス全体でロック同期は実行されません。

この時のListの内容は図の通りで、要素1、2、3の3つが入っているとします。

ここに画像の説明を挿入します

ステップ A と B の実行時にはロックがないため、スレッド x がステップ A の実行を終了した後、ステップ B を実行する前に、別のスレッド y が削除操作を実行する可能性があります。要素 1 が削除されるとします。

削除操作は、最初に排他ロックを取得し、次にコピーオンライト操作を実行します。つまり、現在の配列のコピーをコピーし、コピーされた配列の get メソッドを通じてスレッド x がアクセスしたい要素 1 を削除します。配列を作成し、その配列がコピーされた配列を指すようにします。

このとき、スレッド x がまだ使用しているため、配列が指す配列の参照カウントは 0 ではなく 1 になります。このとき、スレッド x はステップ B の実行を開始します。ステップ B で操作される配列は、前の配列です。スレッド y は要素を削除します。

ここに画像の説明を挿入します
したがって、スレッド y がインデックスの要素を削除しても、スレッド x のステップ B は依然としてインデックスの要素を返します。これは実際には、コピーオンライト戦略によって引き起こされる弱い整合性の問題です。

指定された要素を変更します

E set(int index, E element) を使用して、リスト内の指定された要素の値を変更します。指定された位置にある要素が存在しない場合は、IndexOutOfBoundsException 例外がスローされます。

ここに画像の説明を挿入します
上記のコードは、最初に排他ロックを取得して他のスレッドによる配列の変更を防ぎ、次に現在の配列を取得し、get メソッドを呼び出して指定された位置の要素を取得します。値を指定すると、新しい配列が作成され、要素がコピーされます。その後、新しい配列の指定された位置にある要素の値を変更し、新しい配列を array に設定します。

指定された位置の要素の値が新しい値と同じである場合、揮発性セマンティクスを保証するために、配列の内容は変更されていませんが、配列をリセットする必要があります。

要素の削除

リスト内の指定された要素を削除するには、E Remove(int Index)、boolean Remove(Object o)、および boolean Remove(Object o, Object[] snapshot, int Index) などのメソッドを使用できます。それらの原理は同じです。 。以下にremove(int Index)メソッドについて説明します。

ここに画像の説明を挿入します
上記のコードは実際には新しい要素を追加するコードに似ており、まず排他ロックを取得してデータの削除中に他のスレッドが配列を変更できないようにし、次に配列内の削除する要素を取得し、残りの要素をコピーします。要素を新しい配列に追加して使用し、新しい配列で元の配列を置き換え、最終的にロックが解放されてから返されます。

一貫性の低いイテレータ

イテレータはリスト要素を走査するために使用できます。イテレータの弱い整合性とは何かを説明する前に、イテレータの使用方法を示す例を示します。

ここに画像の説明を挿入します
イテレータの hasNext メソッドは、リスト内にまだ要素があるかどうかを判断するために使用され、next メソッドは具体的に要素を返します。CopyOnWriteArrayList のイテレータの弱い一貫性を見てみましょう。いわゆる弱い一貫性とは、イテレータが返された後、他のスレッドによるリストへの追加、削除、および変更がイテレータには認識されないことを意味します。これを見てください。その方法。

ここに画像の説明を挿入します
イテレータを取得するために iterator() メソッドを呼び出すと、COWIterator オブジェクトが実際に返されます。COWIterator オブジェクトのスナップショット変数は現在のリストの内容を保存し、カーソルはリストを走査するときのデータの添え字になります。

スナップショットがリストのスナップショットであると言われるのはなぜですか? これは明らかに、コピーではなく、ポインターによって渡される参照です。

スレッドが返された反復子を使用して要素を走査している間に、他のスレッドがリストを追加、削除、または変更しない場合、それらは参照関係であるため、スナップショット自体はリストの配列になります。

ただし、トラバーサル中に他のスレッドがリストを追加、削除、または変更した場合、リスト内の配列は追加、削除、または変更後に新しい配列に置き換えられ、古い配列が参照されるため、スナップショットはスナップショットになります。この時のスナップショットによると。

これは、イテレータを取得した後、イテレータ要素を使用するときに、他のスレッドが 2 つの異なる配列を操作するため、他のスレッドによって行われたリストへの追加、削除、および変更が表示されず、一貫性が弱いことも示しています。

以下は、マルチスレッド下での反復子の弱い整合性の影響を示す例です。
ここに画像の説明を挿入します
ここに画像の説明を挿入します

上記のコードでは、main 関数は最初に arrayList を初期化し、次にスレッドを開始する前に arrayList イテレータを取得します。

サブスレッド threadOne が開始されると、まず arrayList の最初の要素の値が変更され、次に arrayList 内の添え字 2 および 3 を持つ要素が削除されます。

サブスレッドの実行が完了すると、メインスレッドは取得したイテレータを使用して配列要素を走査します。出力結果から、サブスレッドで実行された操作はどれも有効ではなかったことがわかります。これは弱い整合性の現れです。イテレータの。

イテレータを取得する操作は、サブスレッド操作の前に実行する必要があることに注意してください。

おすすめ

転載: blog.csdn.net/zhuyufan1986/article/details/135456564