私は OpenJDKソースコードCopyOnWriteArrayList
を調べましたが、すべての書き込み操作は同じロックによって保護されており、読み取り操作は保護されていないようですまったく。私が理解しているように、JMMでは、変数へのすべてのアクセス(読み取りと書き込みの両方)をロックによって保護する必要があります。そうしないと、並べ替えの影響が発生する可能性があります。
たとえば、set(int, E)
メソッドには次の行が含まれています(ロック中):
_/* 1 */ int len = elements.length;
/* 2 */ Object[] newElements = Arrays.copyOf(elements, len);
/* 3 */ newElements[index] = element;
/* 4 */ setArray(newElements);
_
一方、get(int)
メソッドはreturn get(getArray(), index);
のみを実行します。
JMMの私の理解では、これは、ステートメント1〜4が1-2(new)-4-2(copyOf)-3のように並べ替えられた場合、get
が矛盾した状態で配列を観察する可能性があることを意味します。
JMMを正しく理解していませんか、それともCopyOnWriteArrayList
がスレッドセーフである理由について他に説明はありますか?
基になる配列参照を見ると、volatile
とマークされていることがわかります。書き込み操作が発生すると(上記の抜粋など)、このvolatile
参照はsetArray
を介した最後のステートメントでのみ更新されます。この時点まで、読み取り操作は配列のold copyから要素を返します。
重要な点は、配列の更新はアトミック操作ですであり、したがって、読み取りでは常に一貫した状態の配列が表示されます。
書き込み操作のロックのみを取得することの利点は、読み取りのスループットが向上することです。これは、CopyOnWriteArrayList
の書き込み操作は、リスト全体のコピーを伴うため、非常に遅くなる可能性があるためです。
配列参照の取得はアトミック操作です。したがって、読者は古い配列または新しい配列のいずれかを参照します-どちらの方法でも状態は一貫しています。 (set(int,E)
は、参照を設定する前に新しい配列の内容を計算するため、割り当てが行われたときに配列の整合性が保たれます。)
配列参照自体はvolatile
としてマークされているため、参照された配列の変更を確認するためにリーダーがロックを使用する必要はありません。 (編集:また、volatile
は、割り当てが再配列されないことを保証します。これにより、配列が不整合な状態にある可能性があるときに割り当てが行われます。)
書き込みロックは、同時変更を防ぐために必要です。これにより、アレイに一貫性のないデータが保持されたり、変更が失われたりする可能性があります。
したがって、Java 1.8によれば、以下はCopyOnWriteArrayListでのarrayおよびlockの宣言です)。
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
以下はaddの定義ですCopyOnWriteArrayListのメソッド
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
@Adamskiがすでに言及したように、arrayは揮発性であり、setArrayメソッドを介してのみ更新されます。その後、すべての読み取り専用呼び出しが行われると、更新された値が取得されるため、配列は常に一貫しています。