簡體   English   中英

增加和刪除ConcurrentHashMap的元素

[英]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是哈希圖的默認加載因子) 。

因此,問題是:我是否正確使用removereplacecomputeIfAbsent操作? 我假設它們是對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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM