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

多线程Java程序中HashMap大小不一致的底层原因探究(OpenJDK17)

问题描述

我编写了一个多线程Java程序,每个线程向共享的HashMap中插入唯一键(键由循环索引和线程名组合而成)。但所有线程执行完毕后,HashMap的大小与预期的插入总数(NUM_THREADS * NUM_INSERTIONS)不一致。我知道HashMap并非线程安全,使用ConcurrentHashMap可解决该问题,但希望了解OpenJDK17的HashMap实现中,具体是哪里的竞态条件导致了这种不一致,尤其是扩容(rehash)过程中的问题。

测试代码如下:

public class CurrentHashMapDemo {
    private static final int NUM_THREADS = 5;
    private static final int NUM_INSERTIONS = 100;
    private static HashMap<String, Integer> hashMap = new HashMap<>();

    public static void main(String[] args) throws InterruptedException{
        ExecutorService executorService = Executors.newFixedThreadPool(NUM_THREADS);
        for(int i=0; i< NUM_THREADS; i++){
            executorService.execute(insertRecord());
        }
        executorService.shutdown();
        if(!executorService.isTerminated()){
            Thread.sleep(1000);
        }
        System.out.println("Size of the hashmap="+ hashMap.size());
    }

    private static Runnable insertRecord(){
        return () -> {
            for(int i=0; i<NUM_INSERTIONS; i++){
                System.out.println("Key:"+ i+Thread.currentThread().getName());
                hashMap.put(i+Thread.currentThread().getName(), i);
            }
        };
    }
}

替换为ConcurrentHashMap后问题即可解决,但我需要明确为何HashMap在多线程环境下无法正常工作,以及导致大小不一致的具体原因。


原因分析

在OpenJDK17的HashMap实现中,多线程环境下的竞态条件主要体现在以下几个方面,直接导致元素丢失、size统计错误:

1. 普通put操作的竞态

当多个线程同时执行put时,即使键不重复,也可能出现覆盖或元素丢失:

  • HashMap的put流程中,会先计算哈希值找到桶位,然后检查桶内是否存在目标键。如果不存在,会创建新节点插入到桶中(链表或红黑树)。
  • 若两个线程同时在同一个桶位执行插入操作,可能会出现后插入的节点覆盖先插入的节点的情况:线程A刚计算好插入位置还未完成节点挂载,线程B已经完成了该位置的节点插入,最终线程A的插入操作会覆盖线程B的节点,导致B插入的元素丢失。

2. 扩容(rehash)过程中的致命竞态

这是导致元素丢失最常见的场景,OpenJDK17的HashMap扩容逻辑中存在多个线程不安全的点:

(1)扩容触发条件的竞态

HashMap会在size > threshold(threshold = capacity * loadFactor)时触发扩容。多个线程同时执行put时,可能都检测到当前size超过阈值,从而同时触发扩容操作。

  • 两个线程各自创建新的扩容后的数组(newTab),然后各自将旧数组的元素迁移到自己的newTab中。最终只有最后完成扩容的线程会将HashMap的table引用指向自己的newTab,另一个线程迁移的所有元素都会丢失。

(2)链表迁移时的节点丢失

在扩容迁移链表元素时,HashMap会将原链表拆分为两个链表(根据哈希值的高位决定放入新数组的哪个桶),然后将这两个链表分别挂载到新数组的对应桶位。

  • 若多个线程同时迁移同一个链表,可能会出现链表节点被重复处理或遗漏的情况。比如线程A正在处理链表的某个节点,线程B同时移动了该节点到新数组,导致线程A后续的处理逻辑混乱,部分节点没有被正确迁移到新数组中,最终这些节点丢失。

(3)size变量的无原子性更新

HashMap的size变量是普通的int类型,没有用volatile修饰,也没有通过原子操作更新。多个线程同时执行put时,会对size进行size++操作,这是一个非原子操作(读-改-写)。

  • 比如线程A和线程B同时读取到size=10,各自执行加1后写回,最终size变成11而不是12,导致size统计值小于实际插入的元素数(甚至在元素丢失的情况下,size可能比实际存在的元素数还大)。

3. 其他潜在问题

除了上述场景,还有比如红黑树转换、节点查找时的竞态,都可能导致元素无法被正确存储或读取,但最直接导致size不一致的还是前面提到的put操作覆盖、扩容时的元素丢失以及size的非原子更新。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 22:41:06