web-dev-qa-db-ja.com

エントリを削除しながらConcurrentHashMapを反復処理します

次のように、エントリを削除しながらConcurrentHashMapを定期的に繰り返したいと思います。

_for (Iterator<Entry<Integer, Integer>> iter = map.entrySet().iterator(); iter.hasNext(); ) {
    Entry<Integer, Integer> entry = iter.next();
    // do something
    iter.remove();
}
_

問題は、反復中に別のスレッドが値を更新または変更している可能性があることです。その場合、スレッドは反復中に古い値しか表示しないため、これらの更新は永久に失われる可能性がありますが、remove()はライブエントリを削除します。

いくつか検討した後、私はこの回避策を思いつきました:

_map.forEach((key, value) -> {
    // delete if value is up to date, otherwise leave for next round
    if (map.remove(key, value)) {
        // do something
    }
});
_

これに関する1つの問題は、equals()AtomicIntegerなど)を実装しない可変値への変更をキャッチしないことです。同時変更で安全に削除するためのより良い方法はありますか?

10
shmosel

回避策は機能しますが、1つの潜在的なシナリオがあります。特定のエントリに一定の更新がある場合、map.remove(key、value)は、更新が終了するまでtrueを返さない場合があります。

JDK8を使用する場合は、ここに私の解決策があります

for (Iterator<Entry<Integer, Integer>> iter = map.entrySet().iterator(); iter.hasNext(); ) {
    Entry<Integer, Integer> entry = iter.next();
    Map.compute(entry.getKey(), (k, v) -> f(v));
    //do something for prevValue
}
....
private Integer prevValue;

private Integer f(Integer v){
    prevValue = v;
    return null;
}

compute()は値にf(v)を適用し、この場合は値をグローバル変数に割り当ててエントリを削除します。

Javadocによると、これはアトミックです。

指定されたキーとその現在のマップ値(または現在のマッピングがない場合はnull)のマッピングを計算しようとします。メソッド呼び出し全体がアトミックに実行されます。他のスレッドによってこのマップで試行された更新操作の一部は、計算の進行中にブロックされる可能性があるため、計算は短く単純である必要があり、このマップの他のマッピングを更新しようとしてはなりません。

1
xz2145

あなたの回避策は実際にはかなり良いです。他にも似たようなソリューションを構築できる機能がありますが(たとえば、computeIfPresent()とトゥームストーンの値を使用)、それぞれに注意点があり、少し異なるユースケースで使用しました。

マップ値にequals()を実装しない型を使用する場合は、対応する型の上に独自のラッパーを使用できます。これは、オブジェクトの同等性のためのカスタムセマンティクスをConcurrentMapによって提供されるアトミック置換/削除操作に挿入する最も簡単な方法です。

更新

ConcurrentMap.remove(Object key, Object value)APIの上に構築する方法を示すスケッチを次に示します。

  • 値に使用する可変タイプの上にラッパータイプを定義し、現在の可変値の上に構築するカスタムequals()メソッドも定義します。
  • BiConsumerforEachに渡すラムダ)で、値のディープコピー(新しいラッパータイプのタイプ)を作成し、値が必要かどうかを判断するロジックを実行しますコピーから削除されます。
  • 値を削除する必要がある場合は、remove(myKey, myValueCopy)。を呼び出します。
    • 値を削除する必要があるかどうかを計算しているときに同時に変更があった場合、remove(myKey, myValueCopy)falseを返します(別のトピックであるABA問題を除く)。

これを説明するコードは次のとおりです。

_import Java.util.Random;
import Java.util.concurrent.ConcurrentHashMap;
import Java.util.concurrent.ConcurrentMap;
import Java.util.concurrent.atomic.AtomicInteger;

public class Playground {

   private static class AtomicIntegerWrapper {
      private final AtomicInteger value;

      AtomicIntegerWrapper(int value) {
         this.value = new AtomicInteger(value);
      }

      public void set(int value) {
         this.value.set(value);
      }

      public int get() {
         return this.value.get();
      }

      @Override
      public boolean equals(Object obj) {
         if (this == obj) {
            return true;
         }
         if (!(obj instanceof AtomicIntegerWrapper)) {
            return false;
         }
         AtomicIntegerWrapper other = (AtomicIntegerWrapper) obj;
         if (other.value.get() == this.value.get()) {
            return true;
         }
         return false;
      }

      public static AtomicIntegerWrapper deepCopy(AtomicIntegerWrapper wrapper) {
         int wrapped = wrapper.get();
         return new AtomicIntegerWrapper(wrapped);
      }
   }

   private static final ConcurrentMap<Integer, AtomicIntegerWrapper> MAP
         = new ConcurrentHashMap<>();

   private static final int NUM_THREADS = 3;

   public static void main(String[] args) throws InterruptedException {
      for (int i = 0; i < 10; ++i) {
         MAP.put(i, new AtomicIntegerWrapper(1));
      }

      Thread.sleep(1);

      for (int i = 0; i < NUM_THREADS; ++i) {
         new Thread(() -> {
            Random rnd = new Random();
            while (!MAP.isEmpty()) {
               MAP.forEach((key, value) -> {
                  AtomicIntegerWrapper elem = MAP.get(key);
                  if (elem == null) {
                     System.out.println("Oops...");
                  } else if (elem.get() == 1986) {
                     elem.set(1);
                  } else if ((rnd.nextInt() & 128) == 0) {
                     elem.set(1986);
                  }
               });
            }
         }).start();
      }

      Thread.sleep(1);

      new Thread(() -> {
         Random rnd = new Random();
         while (!MAP.isEmpty()) {
            MAP.forEach((key, value) -> {
               AtomicIntegerWrapper elem =
                     AtomicIntegerWrapper.deepCopy(MAP.get(key));
               if (elem.get() == 1986) {
                  try {
                     Thread.sleep(10);
                  } catch (Exception e) {}
                  boolean replaced = MAP.remove(key, elem);
                  if (!replaced) {
                     System.out.println("Bailed out!");
                  } else {
                     System.out.println("Replaced!");
                  }
               }
            });
         }
      }).start();
   }
}
_

「Replaced!」と混合された「Bailedout!」のプリントアウトが表示されます。 (気になる同時更新がなかったため、削除は成功しました)、計算はある時点で停止します。

  • カスタムのequals()メソッドを削除してコピーを引き続き使用すると、コピーがマップ内の値と等しいとは見なされないため、「Bailedout!」の無限のストリームが表示されます。
  • コピーを使用しない場合、「救済されました!」は表示されません。印刷すると、説明している問題が発生します。同時変更に関係なく、値は削除されます。
1

あなたが持っているオプションを考えてみましょう。

  1. isUpdated()操作を使用して独自のコンテナクラスを作成し、独自の回避策を使用します。

  2. マップに含まれる要素がわずかで、プット/削除操作と比較してマップを頻繁に反復している場合。 CopyOnWriteArrayListCopyOnWriteArrayList<Entry<Integer, Integer>> lookupArray = ...;を使用することをお勧めします

  3. 他のオプションは、独自のCopyOnWriteMapを実装することです。

    public class CopyOnWriteMap<K, V> implements Map<K, V>{
    
        private volatile Map<K, V> currentMap;
    
        public V put(K key, V value) {
            synchronized (this) {
                Map<K, V> newOne = new HashMap<K, V>(this.currentMap);
                V val = newOne.put(key, value);
                this.currentMap = newOne; // atomic operation
                return val;
            }
        }
    
        public V remove(Object key) {
            synchronized (this) {
                Map<K, V> newOne = new HashMap<K, V>(this.currentMap);
                V val = newOne.remove(key);
                this.currentMap = newOne; // atomic operation
                return val;
            }
        }
    
        [...]
    }
    

マイナスの副作用があります。コピーオンライトコレクションを使用している場合、更新が失われることはありませんが、以前に削除されたエントリが再び表示されます。

最悪の場合:マップがコピーされるたびに、削除されたエントリが復元されます。

0
dieter