ConcurrentHashMapを使用して、1つのスレッドがマップからいくつかのアイテムを定期的に削除し、他のスレッドがマップからアイテムを同時に配置および取得できるようにしたいと思います。
削除スレッドでmap.entrySet().removeIf(lambda)
を使用しています。私はその振る舞いについてどのような仮定をすることができるのだろうかと思っています。 removeIf
メソッドがイテレータを使用してマップ内の要素を調べ、指定された条件を確認し、必要に応じてiterator.remove()
を使用してそれらを削除していることがわかります。
ドキュメントには、ConcurrentHashMapイテレータの動作に関する情報が記載されています。
同様に、イテレーター、スプリッター、および列挙型は、イテレーター/列挙型の作成時または作成以降のある時点でのハッシュテーブルの状態を反映する要素を返します。 ConcurrentModificationExceptionをスローしないでください。ただし、イテレータは、一度に1つのスレッドのみが使用するように設計されています。
removeIf
呼び出し全体が1つのスレッドで発生するため、イテレーターが一度に複数のスレッドで使用されていないことを確認できます。それでも、以下に説明する一連のイベントが可能かどうか疑問に思っています。
'A'->0
_map.entrySet().removeIf(entry->entry.getValue()==0)
の実行が開始されますremoveIf
呼び出し内の.iteratator()
を呼び出し、コレクションの現在の状態を反映するイテレータを取得しますmap.put('A', 1)
を実行します'A'->0
_マッピングが表示され(イテレータは古い状態を反映します)、_0==0
_がtrueであるため、マップからAキーを削除することにします。'A'->1
_が含まれるようになりましたが、スレッドを削除すると、_0
_の古い値が表示され、_'A' ->1
_エントリは削除されるべきではありません。マップは空です。この動作は、さまざまな方法で実装によって防止される可能性があると想像できます。たとえば、イテレータはput/remove操作を反映していないが、常に値の更新を反映している場合や、イテレータのremoveメソッドが、キーでremoveを呼び出す前に、マッピング全体(キーと値の両方)がマップにまだ存在するかどうかを確認する場合があります。私はそれらの出来事についての情報を見つけることができませんでした、そして私はそのユースケースを安全にする何かがあるかどうか疑問に思います。
私も自分のマシンでそのようなケースを再現することができました。問題は、EntrySetView
(ConcurrentHashMap.entrySet()
によって返される)がremoveIf
の実装をCollection
から継承することであり、次のようになります。
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
// `test` returns `true` for some entry
if (filter.test(each.next())) {
// entry has been just changed, `test` would return `false` now
each.remove(); // ...but we still remove
removed = true;
}
}
return removed;
}
私の謙虚な意見では、これはConcurrentHashMap
の正しい実装とは見なされません。
Zieluの回答の下にあるコメントでユーザーZieluと話し合った後、ConcurrentHashMapコードを詳しく調べて、次のことを発見しました。
remove(key, value)
を呼び出すreplaceNode(key, null, value)
メソッドを提供しますreplaceNode
は、削除する前にキーと値の両方がマップにまだ存在するかどうかを確認するため、使用しても問題ありません。ドキュメントによると* null以外の場合、cvの一致を条件として、ノード値をvに置き換えます。
.entrySet()
が呼び出され、EntrySetView
クラスが返されます。次に、removeIf
メソッドは.iterator()
を呼び出し、EntryIterator
を返します。EntryIterator
はBaseIterator
を拡張し、map.replaceNode(p.key, null, null)
を呼び出すremove
実装を継承します。これにより、条件付き削除が無効になり、常にキーが削除されます。イテレータが常に「現在の」値を繰り返し処理し、一部の値が変更されても古い値を返さない場合は、イベントの負の経過を防ぐことができます。それが起こるかどうかはまだわかりませんが、以下のテストケースですべてが検証されているようです。
これで、私の質問で説明した動作が実際に発生する可能性があることを示すテストケースが作成されたと思います。コードに間違いがあれば訂正してください。
コードは2つのスレッドを開始します。それらの1つ(DELETING_THREAD)は、「false」ブール値にマップされたすべてのエントリを削除します。もう1つ(ADDING_THREAD)は、(1, true)
または(1,false)
の値をランダムにマップに配置します。値にtrue
を入れると、チェックされたときにエントリがまだ存在することが期待され、そうでない場合は例外がスローされます。ローカルで実行すると、すぐに例外がスローされます。
package test;
import Java.util.Random;
import Java.util.concurrent.ConcurrentHashMap;
public class MainClass {
private static final Random RANDOM = new Random();
private static final ConcurrentHashMap<Integer, Boolean> MAP = new ConcurrentHashMap<Integer, Boolean>();
private static final Integer KEY = 1;
private static final Thread DELETING_THREAD = new Thread() {
@Override
public void run() {
while (true) {
MAP.entrySet().removeIf(entry -> entry.getValue() == false);
}
}
};
private static final Thread ADDING_THREAD = new Thread() {
@Override
public void run() {
while (true) {
boolean val = RANDOM.nextBoolean();
MAP.put(KEY, val);
if (val == true && !MAP.containsKey(KEY)) {
throw new RuntimeException("TRUE value was removed");
}
}
}
};
public static void main(String[] args) throws InterruptedException {
DELETING_THREAD.setDaemon(true);
ADDING_THREAD.start();
DELETING_THREAD.start();
ADDING_THREAD.join();
}
}