【问题标题】:Can I use ConcurrentHashMap with Integer for thread safe counters?我可以将 ConcurrentHashMap 与 Integer 一起用于线程安全计数器吗?
【发布时间】:2019-12-26 21:05:13
【问题描述】:

我想有几个柜台,我可以按名称寻址。所以,我可以这样实现:

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.computeIfAbsent("key", k -> new Integer(0));
map.computeIfPresent("key", (k, v) -> v + 1);

它的代码是线程安全的吗?

我是这么认为的,因为ConcurrentHashMap 我们有同步访问,并且设置新的引用也是线程安全的操作。由于安全发布,其他线程会看到这种变化,当我们将存储桶锁留在ConcurrentHashMap 时会发生这种情况。

【问题讨论】:

  • 您不妨使用AtomicInteger。 (另外,Integer.valueOf。)
  • @TomHawtin-tackline 是的,我可以使用它。但我只是想知道,我可以为此目的使用 Integer。
  • 这两个单独的语句是线程安全的,但它们在一起并不是因为你有 2 个调用并且这 2 个调用一起不是原子的。如果您想要这 2 个电话,只需使用 compute 并在 BiFunction 中处理这两种情况。另请查看Map::merge 方法 - 这也可能满足您的要求
  • @michalk hm,但我不需要 2 个操作的原子性。如果多个线程创建计数器,只有一个会成功。
  • 您可以放心地将 new Integer(0) 替换为 0 - 自动装箱会为您解决问题

标签: java multithreading java-8 concurrency concurrenthashmap


【解决方案1】:

是的,它是线程安全的,你可以测试:

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadsafeExample {

    public static void main(String[] args) throws Exception {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        int count = 1000;
        AtomicInteger doneTasks = new AtomicInteger();
        Thread[] threads = new Thread[count];
        for(int i = 0 ; i < threads.length ; ++i) {
            threads[i] = new Thread(() -> {
                map.computeIfAbsent("key", k -> new Integer(0));
                map.computeIfPresent("key", (k, v) -> v + 1);
                doneTasks.incrementAndGet();
            });
        }
        for(int i = 0 ; i < threads.length ; ++i)
            threads[i].start();

        while (doneTasks.get() < count)
            Thread.sleep(3);

        System.out.println("we expected count of key is: " + count + ", and we get: " + map.get("key"));
    }

}

输出:

we expected count of key is: 1000, and we get: 1000

你可以替换:

map.computeIfAbsent("key", k -> new Integer(0));
map.computeIfPresent("key", (k, v) -> v + 1);

通过

map.compute("key", (k, v) -> v == null ? 1 : v + 1);

【讨论】:

  • 你可以使用map.merge("key", 1, (a,b) -&gt; a+b);map.merge("key", 1, Integer::sum);来代替map.compute("key", (k, v) -&gt; v == null ? 1 : v + 1);...
【解决方案2】:

假设第一个语句map.computeIfAbsent("key", k -&gt; new Integer(0)); 发生在“初始化时间”,然后有一堆线程执行map.computeIfPresent("key", (k, v) -&gt; v + 1); 形式的调用,是的,算法将是正确的(我是否理解你的意图?)。

JDK 的最新版本保证对 ConcurrentHashMap.computeIfPresent() 的调用不仅会调用以线程安全方式传入的表达式,它还将保证如果其他线程同时尝试对同一个键进行操作,它们将被阻塞并排队,这样所有的突变都按顺序发生(这在分布式系统的用语中称为可串行化)。

【讨论】:

  • “最新版本”是什么意思?是否有任何版本不能保证这一点?除此之外,map.merge(key, 1, (a,b) -&gt; a+b)map.merge(key, 1, Integer::sum) 将同时用于初始化缺失密钥和更新当前密钥。
  • @Holger:行为从 JDK7 更改为 JDK8,或者从 8 更改为 9,我不记得了。如果我没记错的话,它过去总是通过同步,现在它只会在计算中间确实有一些线程时才会这样做。
  • 该方法在 Java 8 之前不存在,该版本也定义了我们今天仍然拥有的语义。此外,您的陈述相互矛盾。在答案中,您说“JDK 的最新版本……也将保证……”,这意味着某些版本中不存在这种额外的保证,而在评论中,您说“它曾经总是通过同步”,这将是一个潜在的性能问题,但仍提供您提到的所有保证。
猜你喜欢
  • 2020-10-13
  • 2021-04-27
  • 2011-04-15
  • 1970-01-01
  • 1970-01-01
  • 2017-10-18
  • 2012-11-30
  • 2012-08-20
  • 2014-03-04
相关资源
最近更新 更多