增加和删除ConcurrentHashMap的元素

时间:2016-12-19 19:35:40

标签: java multithreading concurrency concurrenthashmap

有一个类Counter,它包含一组键,允许递增每个键的值并获取所有值。所以,我试图解决的任务与Atomically incrementing counters stored in 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基于Use of ConcurrentHashMap eliminates data-visibility troubles?的答案的原子操作。如果是这样,为什么一些增量会丢失?

编辑:

我认为我错过了一个重要的细节,increment()应该比getAndClear()更频繁地调用,以便我尽量避免increment()中的任何显式锁定。但是,我将在稍后测试不同版本的性能,看它是否真的是一个问题。

1 个答案:

答案 0 :(得分:1)

我认为问题是在remove上迭代时使用keySet。这就是JavaDoc对Map#keySet()所说的内容(我的重点):

  

返回此地图中包含的键的Set视图。该集由地图支持,因此对地图的更改将反映在集中,反之亦然。 如果在对集合进行迭代时修改了映射(除非通过迭代器自己的remove操作),迭代的结果是未定义的

ConcurrentHashMap的JavaDoc提供了更多线索:

  

类似地,Iterators,Spliterators和Enumerations在迭代器/枚举的创建时或之后的某个时刻返回反映哈希表状态的元素。

结论是,在迭代键时改变地图是不可预测的。

一种解决方案是为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());
}
}