You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ConcurrentHashMap在多线程选举计票中结果不符预期的问题咨询

问题拆解与解决方案

嘿,我来帮你理清楚这个问题——你踩了线程安全里的一个常见误区:ConcurrentHashMap的单个方法确实是线程安全的,但多个方法组合成的复合操作就不再是原子性的了!

咱们先拆解你代码里的关键操作:

voting.put("GERB", voting.getOrDefault("GERB", 0) + 1);

这一行看起来是一步,但实际在JVM里是三个独立的步骤:

  1. 调用getOrDefault拿到当前"GERB"的票数
  2. 把拿到的数值加1
  3. 调用put把新数值存回Map

虽然getOrDefault和put各自都是线程安全的,但这三个步骤之间没有任何同步措施。当多个线程同时执行这段代码时,就会出现竞态条件:比如线程A刚读到当前票数是10,还没来得及加1存回去,线程B也读到了10,然后两个线程都把11存回去——这就等于白白少加了一次,最终结果自然会比预期的116(16个线程+主线程100次)要少,而且每次运行的丢数情况不一样,所以结果在114-116之间波动。

靠谱的解决方法

方法1:用ConcurrentHashMap的merge方法(最推荐)

merge方法本身就是为这种原子性的"累加"场景设计的,它会帮你把"获取当前值→计算新值→写入新值"整个流程做成原子操作,底层已经处理好了线程同步:

// 线程里的代码改成这样
new Thread(() -> {
    voting.merge("GERB", 1, Integer::sum);
}).start();

// 主线程的循环也同步改成merge
for (int i = 0; i < 100; i++) {
    voting.merge("GERB", 1, Integer::sum);
}

这里的Integer::sum是一个函数式接口,意思是把Map里现有的值和传入的1相加,整个过程完全线程安全,不会出现丢数的情况。

方法2:把Map的值换成AtomicInteger

另一种思路是让Map存储线程安全的计数器类型,比如AtomicInteger,它的incrementAndGet方法是原子性的自增:

// 先把Map的类型改成存储AtomicInteger
Map<String, AtomicInteger> voting = new ConcurrentHashMap<>();
// 初始化GERB的计数器
voting.put("GERB", new AtomicInteger(0));

// 线程里直接调用原子自增
for (int i = 0; i < 16; i++) {
    new Thread(() -> {
        voting.get("GERB").incrementAndGet();
    }).start();
}

// 主线程的循环也一样
for (int i = 0; i < 100; i++) {
    voting.get("GERB").incrementAndGet();
}

这种方式也能保证每一次计数都是原子操作,不会出现竞态条件。

方法3:使用compute方法

compute方法也是原子性的,允许你根据当前键的现有值计算新值并更新,逻辑和merge类似:

new Thread(() -> {
    voting.compute("GERB", (key, currentVal) -> {
        // 如果当前值为null(还没初始化)就返回1,否则加1
        return currentVal == null ? 1 : currentVal + 1;
    });
}).start();

同样,整个计算和更新过程是线程安全的。

额外小提示

你代码里用Thread.sleep(5000)等待线程结束其实不太靠谱——万一线程提前跑完了,就白白浪费时间;万一线程跑慢了,5秒不够就会导致主线程提前输出结果。更好的方式是把线程存起来,用join()等待所有线程完成:

List<Thread> threadList = new ArrayList<>();
for (int i = 0; i < 16; i++) {
    Thread t = new Thread(() -> {
        voting.merge("GERB", 1, Integer::sum);
    });
    threadList.add(t);
    t.start();
}

// 等待所有线程执行完毕
for (Thread t : threadList) {
    try {
        t.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

// 之后再执行主线程的100次操作或者直接输出结果

这样能精准等待所有线程完成任务,不用靠猜睡眠时间。

内容的提问来源于stack exchange,提问作者Vallerious

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 16:38:12