[英]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.