[英]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的任何其他映射。
让我们考虑一下您有哪些选择。
使用isUpdated()
操作创建自己的Container类,并使用自己的解决方法。
如果你的地图只包含几个元素,那么你就可以非常频繁地迭代地图,而不是放置/删除操作。 使用CopyOnWriteArrayList
CopyOnWriteArrayList<Entry<Integer, Integer>> lookupArray = ...;
是一个不错的选择CopyOnWriteArrayList<Entry<Integer, Integer>> lookupArray = ...;
另一种选择是实现自己的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.