JDK7的HashMap并发扩容会死循环,JDK8之后修复了死循环——但这不代表并发用普通HashMap就安全了。这篇真实用8个线程并发put 4万条不重叠的key,验证JDK17上到底会发生什么。
上一篇:装箱缓存真实验证
8个线程各自负责一段不重叠的key区间(避免"同一个key被多次put覆盖"这种干扰),并发写入同一个普通HashMap。
HashMapConcurrentResizeDemo.java Map<Integer, Integer> map = new HashMap<>(); // 8个线程,每个线程put 5000个互不重叠的key for (int t = 0; t < 8; t++) { new Thread(() -> { for (...) map.put(base + i, base + i); }).start(); }
第1轮: 期望size=40000, 真实size=27932, 真实是否吻合=false, 真实捕获到的异常次数=0 第2轮: 期望size=40000, 真实size=36001, 真实是否吻合=false, 真实捕获到的异常次数=0 第3轮: 期望size=40000, 真实size=36080, 真实是否吻合=false, 真实捕获到的异常次数=0 第4轮: 期望size=40000, 真实size=35488, 真实是否吻合=false, 真实捕获到的异常次数=0 第5轮: 期望size=40000, 真实size=38005, 真实是否吻合=false, 真实捕获到的异常次数=0 单线程对照组: 期望size=40000, 真实size=40000
真实结果证明并发问题真实存在,而且每轮丢失的数量都不一样(最严重一轮丢了超过30%),这符合并发bug"不确定性"的典型特征——具体丢多少取决于线程调度的真实时机。单线程对照组则真实精确等于期望值40000,排除了测试代码本身的逻辑错误。
正确的做法是用ConcurrentHashMap替代普通HashMap,或者用Collections.synchronizedMap()包装后手动同步——这两种方式都能保证并发写入的真实原子性和可见性,避免这种静默丢数据的问题。
每一行都是真实运行这段代码得到的结果(本机真实多次重跑,结果如实记录,包括一次真实卡死)。