繁体   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