[英]Incrementing and removing elements of ConcurrentHashMap
有一个Counter
类,其中包含一组键,并允许递增每个键的值并获取所有值。 因此,我要解决的任务与ConcurrentHashMap中存储的以原子方式递增的计数器中的任务相同。 区别在于密钥集是无界的,因此经常添加新密钥。
为了减少内存消耗,我在读取值后清除了这些值,这发生在Counter.getAndClear()
。 密钥也被删除,这似乎使事情分崩离析。
一个线程递增随机键,另一个线程获取所有值的快照并清除它们。
代码如下:
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.Map;
import java.util.HashMap;
import java.lang.Thread;
class HashMapTest {
private final static int hashMapInitSize = 170;
private final static int maxKeys = 100;
private final static int nIterations = 10_000_000;
private final static int sleepMs = 100;
private static class Counter {
private ConcurrentMap<String, Long> map;
public Counter() {
map = new ConcurrentHashMap<String, Long>(hashMapInitSize);
}
public void increment(String key) {
Long value;
do {
value = map.computeIfAbsent(key, k -> 0L);
} while (!map.replace(key, value, value + 1L));
}
public Map<String, Long> getAndClear() {
Map<String, Long> mapCopy = new HashMap<String, Long>();
for (String key : map.keySet()) {
Long removedValue = map.remove(key);
if (removedValue != null)
mapCopy.put(key, removedValue);
}
return mapCopy;
}
}
// The code below is used for testing
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread thread = new Thread(new Runnable() {
public void run() {
for (int j = 0; j < nIterations; j++) {
int index = ThreadLocalRandom.current().nextInt(maxKeys);
counter.increment(Integer.toString(index));
}
}
}, "incrementThread");
Thread readerThread = new Thread(new Runnable() {
public void run() {
long sum = 0;
boolean isDone = false;
while (!isDone) {
try {
Thread.sleep(sleepMs);
}
catch (InterruptedException e) {
isDone = true;
}
Map<String, Long> map = counter.getAndClear();
for (Map.Entry<String, Long> entry : map.entrySet()) {
Long value = entry.getValue();
sum += value;
}
System.out.println("mapSize: " + map.size());
}
System.out.println("sum: " + sum);
System.out.println("expected: " + nIterations);
}
}, "readerThread");
thread.start();
readerThread.start();
thread.join();
readerThread.interrupt();
readerThread.join();
// Ensure that counter is empty
System.out.println("elements left in map: " + counter.getAndClear().size());
}
}
在测试时,我注意到一些增量丢失了。 我得到以下结果:
sum: 9993354
expected: 10000000
elements left in map: 0
如果您无法重现此错误(该总和小于预期),则可以尝试将maxKeys增加几个数量级或减少hashMapInitSize或增加nIterations(后者也会增加运行时间)。 如果有任何错误,我还包括了测试代码(主要方法)。
我怀疑在运行时增加ConcurrentHashMap的容量时会发生错误。 在我的计算机上,当hashMapInitSize
为170时,代码似乎可以正常工作,但是当hashMapInitSize
为171时,代码将失败。我认为171的大小会触发容量增加(128 / 0.75 == 170.66,其中0.75是哈希图的默认加载因子) 。
因此,问题是:我是否正确使用remove
, replace
和computeIfAbsent
操作? 我假设它们是对ConcurrentHashMap
的原子操作,基于对ConcurrentHashMap
的使用的答案, 从而消除了数据可见性麻烦? 。 如果是这样,为什么会丢失一些增量?
编辑:
我想,我错过了一个重要的细节在这里, increment()
是应该更频繁地调用超过getAndClear()
所以我尽量避免任何明确锁定increment()
但是,稍后我将测试不同版本的性能,以查看是否确实存在问题。
我猜问题是在迭代keySet
时使用remove
。 这是JavaDoc对Map#keySet()
所说的(我的重点是):
返回此映射中包含的键的Set视图。 该集合由地图支持,因此对地图的更改会反映在集合中,反之亦然。 如果在对集合进行迭代时修改了映射(通过迭代器自己的remove操作除外),则迭代的结果不确定 。
用于ConcurrentHashMap
的JavaDoc提供了更多线索:
同样,迭代器,拆分器和枚举返回的元素反映了在创建迭代器/枚举时或此后某个时刻哈希表的状态。
结论是,在迭代键的同时对映射进行变异不是可预测的。
一种解决方案是为getAndClear()操作创建一个新地图,然后仅返回旧地图。 开关必须受到保护,在下面的示例中,我使用了ReentrantReadWriteLock
:
class HashMapTest {
private final static int hashMapInitSize = 170;
private final static int maxKeys = 100;
private final static int nIterations = 10_000_000;
private final static int sleepMs = 100;
private static class Counter {
private ConcurrentMap<String, Long> map;
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
ReadLock readLock = lock.readLock();
WriteLock writeLock = lock.writeLock();
public Counter() {
map = new ConcurrentHashMap<>(hashMapInitSize);
}
public void increment(String key) {
readLock.lock();
try {
map.merge(key, 1L, Long::sum);
} finally {
readLock.unlock();
}
}
public Map<String, Long> getAndClear() {
ConcurrentMap<String, Long> oldMap;
writeLock.lock();
try {
oldMap = map;
map = new ConcurrentHashMap<>(hashMapInitSize);
} finally {
writeLock.unlock();
}
return oldMap;
}
}
// The code below is used for testing
public static void main(String[] args) throws InterruptedException {
final AtomicBoolean ready = new AtomicBoolean(false);
Counter counter = new Counter();
Thread thread = new Thread(new Runnable() {
public void run() {
for (int j = 0; j < nIterations; j++) {
int index = ThreadLocalRandom.current().nextInt(maxKeys);
counter.increment(Integer.toString(index));
}
}
}, "incrementThread");
Thread readerThread = new Thread(new Runnable() {
public void run() {
long sum = 0;
while (!ready.get()) {
try {
Thread.sleep(sleepMs);
} catch (InterruptedException e) {
//
}
Map<String, Long> map = counter.getAndClear();
for (Map.Entry<String, Long> entry : map.entrySet()) {
Long value = entry.getValue();
sum += value;
}
System.out.println("mapSize: " + map.size());
}
System.out.println("sum: " + sum);
System.out.println("expected: " + nIterations);
}
}, "readerThread");
thread.start();
readerThread.start();
thread.join();
ready.set(true);
readerThread.join();
// Ensure that counter is empty
System.out.println("elements left in map: " + counter.getAndClear().size());
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.