HashMap死循环有多坑?CPU直接拉满!JDK1.7/1.8底层对比+避坑指南

11 阅读3分钟
  1. 问题描述 做Java开发的,谁没踩过HashMap的坑?线上环境突然CPU 100%,排查半天发现是HashMap并发put导致的死循环!尤其是JDK1.7和1.8的底层实现差异,很多高级开发都容易混淆。今天从原理到实战,带你彻底搞懂HashMap并发问题,附上可运行代码,避坑率100%!
  2. 核心原理
  • JDK1.7:采用头插法+数组+链表结构,扩容时会将原链表节点转移到新数组,并发场景下会形成环形链表,调用get方法时会陷入死循环,直接导致CPU飙升至100%。
  • JDK1.8:优化为尾插法+数组+链表+红黑树结构,扩容时不会形成环形链表,彻底解决死循环问题,但仍不保证线程安全,并发put可能出现数据丢失。
  1. 示例代码
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
/**
 * HashMap并发问题复现与线程安全替代方案
 * 说明:JDK1.7环境下运行会大概率出现死循环,JDK1.8会出现size异常/数据丢失
 * 直接复制可运行,无需修改
 */
public class HashMapConcurrentTest {
    // 非线程安全,并发场景下禁止使用(坑点)
    private static final HashMap<String, String> HASH_MAP = new HashMap<>();
    // 生产级线程安全替代方案,推荐使用
    private static final ConcurrentHashMap<String, String> CONCURRENT_HASH_MAP = new ConcurrentHashMap<>();

    public static void main(String[] args) throws InterruptedException {
        // 模拟20个线程并发put,每个线程执行1000次put操作
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    // 不安全的put操作
                    HASH_MAP.put(Thread.currentThread().getName() + j, "val" + j);
                    // 安全的put操作
                    CONCURRENT_HASH_MAP.put(Thread.currentThread().getName() + j, "val" + j);
                }
            }).start();
        }
        // 等待3秒,让线程执行完成
        Thread.sleep(3000);
        // 安全map:size正常,无数据丢失
        System.out.println("ConcurrentHashMap size:" + CONCURRENT_HASH_MAP.size());
        // 不安全map:size异常(小于20000),JDK1.7会直接死循环
        System.out.println("HashMap size:" + HASH_MAP.size());
    }
}
  1. 常见问题&解决方案

问题1:线上并发put后,HashMap的size不对,甚至出现CPU 100%? 解决:禁止在并发场景下使用HashMap,生产环境直接替换为ConcurrentHashMap,无需自己手动加锁,效率更高。

原因:

  • HashMap 非线程安全
  • 多线程并发 put 会造成:
    • size 统计错误
    • 链表成环 → CPU 100%
  • JDK 1.7 尤其严重,JDK 1.8 仍不保证安全

错误代码示例:

import java.util.HashMap;
import java.util.concurrent.CountDownLatch;

public class HashMapConcurrentBug {
    public static void main(String[] args) throws InterruptedException {
        HashMap<String, String> map = new HashMap<>();
        int threadCount = 10;
        CountDownLatch latch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            int finalI = i;
            new Thread(() -> {
                // 并发 put
                for (int j = 0; j < 1000; j++) {
                    map.put("key-" + finalI + "-" + j, "val");
                }
                latch.countDown();
            }).start();
        }
        latch.await();

        // 预期:10*1000=10000
        // 实际:永远小于 10000,甚至卡死
        System.out.println("map.size() = " + map.size());
    }
}

正确代码示例:

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;

public class ConcurrentHashMapFix {
    public static void main(String[] args) throws InterruptedException {
        // 替换这一行即可
        Map<String, String> map = new ConcurrentHashMap<>();

        int threadCount = 10;
        CountDownLatch latch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            int finalI = i;
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    map.put("key-" + finalI + "-" + j, "val");
                }
                latch.countDown();
            }).start();
        }
        latch.await();

        // 结果一定是 10000,安全、高效、无锁竞争
        System.out.println("map.size() = " + map.size());
    }
}

问题2:JDK1.8的HashMap,查询速度突然变慢,怀疑红黑树退化? 解决:合理设置HashMap初始容量(推荐初始容量=预计元素数/0.75 +1,默认16),避免频繁扩容导致红黑树与链表频繁转换。

原因:

频繁扩容 → 链表和红黑树来回转换

树化阈值:链表长度 ≥8 且容量 ≥64 才树化

频繁 resize 会让红黑树退化成链表,查询从 O (1) → O (n)

代码示例:

import java.util.HashMap;

public class HashMapCapacityFix {
    public static void main(String[] args) {
        // 预计存储 1000 条数据
        int expectedSize = 1000;
        
        // 正确计算初始容量
        int initialCapacity = (int) (expectedSize / 0.75) + 1;
        System.out.println("推荐初始容量:" + initialCapacity);

        HashMap<String, String> map = new HashMap<>(initialCapacity);

        for (int i = 0; i < expectedSize; i++) {
            map.put("key-" + i, "val");
        }
        
        // 无频繁扩容、无红黑树退化、查询稳定 O(1)
        System.out.println("map.size() = " + map.size());
    }
}

HashMap的并发坑是Java后端高频面试题,也是线上故障高发点,建议收藏本文,下次遇到类似问题直接套用解决方案!觉得有用的话,点赞+关注