ComputeIfAbsent 错误的地图大小

问题描述 投票:0回答:2

我已经尝试了下面的代码,以便为每个 jj 提供一个唯一的 id。

据我所知,computeIfAbsent 是线程安全的但是:

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService =
                Executors.newFixedThreadPool(4);

        final Map<String, ConcurrentHashMap<String, Short>> threadSafeMap = new ConcurrentHashMap<>();
        threadSafeMap.put("0", new ConcurrentHashMap<>());
        threadSafeMap.put("1", new ConcurrentHashMap<>());

        for (int i = 1; i <= 10; i++) {
            final int jj = i;
            executorService.submit(() -> {
                int                                key              = jj % 2;
                final ConcurrentMap<String, Short> idByName = threadSafeMap.get(String.valueOf(key));
                return idByName.computeIfAbsent(String.valueOf(jj), x -> (short) idByName.size());
            });
        }
        executorService.shutdown();
        executorService.awaitTermination(5, TimeUnit.SECONDS);
        System.out.println(threadSafeMap);
    }

实际价值: {0={2=0, 4=0, 6=2, 8=3, 10=4}, 1={1=0, 3=0, 5=2, 7=3, 9=4}}

期望值例如(由于并发): {0={2=0, 4=1, 6=2, 8=3, 10=4}, 1={1=1, 3=0, 5=2, 7=3, 9=4}}

问题是我有 2=0 和 4=0,这是错误的值应该是唯一的!

顺便说一句,使用整数而不是短整数解决了这个问题! 你能帮忙吗?

java multithreading concurrency java.util.concurrent concurrenthashmap
2个回答
2
投票

您的假设是错误的,因为许多不同的线程可能同时为不同的键执行相同的映射函数。

ConcurrentHashMap
是线程安全的,但允许并发更新地图。一些访问底层映射表相似部分的调用者可能会在等待另一个线程完成时阻塞。

你不会发现

computeIfAbsent(someKey,mapFunc)
对同一个键运行映射函数两次,因为它是对那个键的原子操作。所以第二个或并发调用者会看到第一个调用的值。然而另一个
computeIfAbsent(anotherKey,mapFunc)
可能同时运行。

javadoc 声明:

如果密钥不存在,则每次调用此方法时仅调用一次提供的函数,否则根本不调用。

在计算过程中,其他线程在此映射上的一些尝试更新操作可能会被阻塞,因此计算应该简短。

如果您希望拥有独特的价值,您可以使用

AtomicInteger
counter.


0
投票

ConcurrentHashMap 需要注意的是映射大小是如何计算的:

public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 :
            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
            (int)n);
}

final long sumCount() {
    CounterCell[] cs = counterCells;
    long sum = baseCount;
    if (cs != null) {
        for (CounterCell c : cs)
            if (c != null)
                sum += c.value;
    }
    return sum;
}

这些“计数器单元”在内部调用

addCount
时更新。 #computeIfAbsent
方法的最后
我们有:

if (val != null)
    addCount(1L, binCount);
return val;

但是,在到达此代码之前,您传递的 lambda 表达式已经在该方法的前面进行了评估。例如,当我们第一次添加值时,它们将在

ReservationNode
:

上同步
Node<K,V> r = new ReservationNode<K,V>();
synchronized (r) {
    if (casTabAt(tab, i, null, r)) { //compare-and-swap
        binCount = 1;
        Node<K,V> node = null;
        try {
            if ((val = mappingFunction.apply(key)) != null) //runs the mapping function
                node = new Node<K,V>(h, key, val);
            } finally {
                setTabAt(tab, i, node);
            }
        }
    }
}

src: ConcurrentHashMap#computeIfAbsent

上面的代码对我来说很可疑,让我觉得这里的创建阶段不会真正阻塞/同步任何东西。稍后在该方法中(一旦表条目存在),您会看到它执行

synchronized(f)
(哈希桶)以计算/放置新值,然后导致您在上面看到的顺序插入。

因此,那些在开始时(当我们第一次初始化地图时)的并发更新不仅能够并行运行,而且即使适当的同步也可能在地图结束之前检索

0
地图的大小
#computeIfAbsent
来电

© www.soinside.com 2019 - 2024. All rights reserved.