繁体   English   中英

在删除条目时迭代ConcurrentHashMap

[英]Iterate over ConcurrentHashMap while deleting entries

我想在删除条目时定期迭代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
    }
});

这样做的一个问题是它不会捕获对未实现equals()可变值的修改(例如AtomicInteger )。 是否有更好的方法可以安全删除并发修改?

你的解决方法实际上非常好。 还有其他设施,你可以建立一个有点类似的解决方案(例如使用computeIfPresent()和墓碑值),但他们有自己的警告,我已经在略有不同的用例中使用它们。

至于使用不对map值实现equals()的类型,可以在相应类型的顶部使用自己的包装器。 这是将对象相等的自定义语义注入ConcurrentMap提供的原子替换/删除操作的最直接方法。

更新

这是一个草图,展示了如何在ConcurrentMap.remove(Object key, Object value) API之上构建:

  • 在用于值的可变类型之上定义包装类型,还定义在当前可变值之上构建的自定义equals()方法。
  • 在你的BiConsumer (你传递给forEach的lambda)中,创建一个值的深层副本(它是你的新包装类型的类型)并执行你的逻辑,确定是否需要在副本上删除该值。
  • 如果需要删除该值,请调用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();
   }
}

你会看到“Bailed out!”的打印输出,混合了“Replaced!” (删除成功,因为没有您关心的并发更新),计算将在某个时刻停止。

  • 如果你删除自定义的equals()方法并继续使用副本,你会看到一个源源不断的“Bailed out!”,因为副本永远不会被认为等于地图中的值。
  • 如果你不使用副本,你将看不到“被勒死!” 打印出来,你会遇到你正在解释的问题 - 无论并发更改,都会删除值。

您的解决方法有效,但有一种可能的情况。 如果某些条目具有常量更新,则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)。 整个方法调用以原子方式执行。 其他线程在此映射上的某些尝试更新操作可能在计算进行时被阻止,因此计算应该简短,并且不得尝试更新此Map的任何其他映射。

让我们考虑一下您有哪些选择。

  1. 使用isUpdated()操作创建自己的Container类,并使用自己的解决方法。

  2. 如果你的地图只包含几个元素,那么你就可以非常频繁地迭代地图,而不是放置/删除操作。 使用CopyOnWriteArrayList CopyOnWriteArrayList<Entry<Integer, Integer>> lookupArray = ...;是一个不错的选择CopyOnWriteArrayList<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; } } [...] } 

有副作用。 如果您使用的是写时复制集合,您的更新将永远不会丢失,但您可以再次看到一些以前删除的条目。

最坏情况:如果复制了地图,则每次都会恢复已删除的条目。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM