我希望在删除条目时定期迭代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
)。是否有更好的方法可以通过并发修改安全删除?
答案 0 :(得分:1)
您的解决方法实际上非常好。还有其他设施,您可以在其上建立一个类似的解决方案(例如使用computeIfPresent()
和墓碑值),但他们有自己的警告,我在稍微不同的用例中使用它们。
对于使用不对地图值实现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();
}
}
您会看到&#34;打印出来的打印输出!&#34;,与&#34;混合替换!&#34; (删除成功,因为没有您关心的并发更新),计算将在某个时刻停止。
equals()
方法并继续使用副本,您将看到源源不断的&#34;保释!&#34;,因为该副本从未被视为相同到地图中的值。答案 1 :(得分: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)。整个方法调用以原子方式执行。其他线程在此映射上尝试的某些更新操作可能会在计算过程中被阻止,因此计算应该简短,并且不得尝试更新此Map的任何其他映射。
答案 2 :(得分:0)
让我们考虑一下您有哪些选择。
使用isUpdated()
操作创建您自己的Container类,并使用您自己的解决方法。
如果你的地图只包含几个元素,那么你就可以非常频繁地在地图上进行迭代,并与put / delete操作进行比较。使用CopyOnWriteArrayList
可能是个不错的选择
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;
}
}
[...]
}
存在负面影响。如果您使用的是写时复制集合,您的更新将永远不会丢失,但您可以再次看到一些以前删除的条目。
最坏情况:如果地图被复制,每次都会恢复已删除的条目。