ConcurrentHashMap在多线程选举计票中结果不符预期的问题咨询
嘿,我来帮你理清楚这个问题——你踩了线程安全里的一个常见误区:ConcurrentHashMap的单个方法确实是线程安全的,但多个方法组合成的复合操作就不再是原子性的了!
咱们先拆解你代码里的关键操作:
voting.put("GERB", voting.getOrDefault("GERB", 0) + 1);
这一行看起来是一步,但实际在JVM里是三个独立的步骤:
- 调用
getOrDefault拿到当前"GERB"的票数 - 把拿到的数值加1
- 调用
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

