每日面试题-假设有一个 1G 大的 HashMap,此时用户请求过来刚好触发它的扩容,会怎样?让你改造下 HashMap 的实现该怎样优化?

一、原理解析:HashMap 扩容机制的核心问题

当 HashMap 的 size > capacity * loadFactor 时触发扩容(默认负载因子 0.75)。扩容流程如下:

  1. 创建新数组:容量翻倍(newCap = oldCap << 1)。
  2. 数据迁移:遍历旧数组的所有 Entry,重新计算哈希分配到新数组。
  3. 资源消耗:时间复杂度 O(n),内存瞬间翻倍(新旧数组共存)。
1G HashMap 扩容时的典型问题:
  • 内存抖动:瞬时内存需求从 1G 增长到 3G(旧数组 1G + 新数组 2G),可能触发 OOM。
  • 长 STW(Stop-The-World):单线程扩容耗时可达秒级(假设每秒处理 1M Entry,1G 需约 17 分钟)。
  • 服务不可用:扩容期间所有写操作被阻塞,高并发场景下请求堆积。

二、优化方案设计

以下是可落地的优化方案及代码示例:

方案 1:渐进式扩容(Incremental Resizing)

核心思想:将一次性全量迁移拆分为多次小批量迁移,分摊计算压力。

public class IncrementalHashMap {
    private Node[] oldTable;
    private Node[] newTable;
    private volatile int migrationIndex = 0; // 迁移进度指针

    public void put(K key, V value) {
        if (isResizing()) {
            // 触发渐进式迁移(每次迁移 10 个桶)
            migrateStep(10);
        }
        // 正常插入逻辑(优先插入新表)
        insertIntoNewTable(key, value);
    }

    private void migrateStep(int steps) {
        for (int i = 0; i < steps && migrationIndex < oldTable.length; i++, migrationIndex++) {
            Node entry = oldTable[migrationIndex];
            while (entry != null) {
                insertIntoNewTable(entry.key, entry.value);
                entry = entry.next;
            }
            oldTable[migrationIndex] = null; // 清空旧桶
        }
        if (migrationIndex >= oldTable.length) {
            oldTable = null; // 完成迁移
        }
    }
}
方案 2:分片哈希(Sharded HashMap)

核心思想:将大 Map 拆分为多个小 Map,降低单次扩容影响。

public class ShardedHashMap {
    private final HashMap[] shards;
    private static final int SHARD_COUNT = 16; // 分片数(按业务调整)

    public ShardedHashMap() {
        shards = new HashMap[SHARD_COUNT];
        for (int i = 0; i < SHARD_COUNT; i++) {
            shards[i] = new HashMap<>();
        }
    }

    private int getShardIndex(K key) {
        return key.hashCode() % SHARD_COUNT;
    }

    public V put(K key, V value) {
        int shardIndex = getShardIndex(key);
        synchronized (shards[shardIndex]) { // 细粒度锁
            return shards[shardIndex].put(key, value);
        }
    }
}
方案 3:动态负载因子(Adaptive Load Factor)

核心思想:根据系统负载动态调整扩容阈值。

public class AdaptiveHashMap extends HashMap {
    private double currentLoadFactor = 0.75;

    @Override
    public V put(K key, V value) {
        adjustLoadFactorBasedOnMemoryPressure();
        return super.put(key, value);
    }

    private void adjustLoadFactorBasedOnMemoryPressure() {
        Runtime runtime = Runtime.getRuntime();
        long freeMemory = runtime.freeMemory();
        long totalMemory = runtime.totalMemory();
        
        // 内存紧张时提高负载因子(示例逻辑)
        if (freeMemory < totalMemory * 0.1) {
            currentLoadFactor = Math.min(0.95, currentLoadFactor + 0.05);
        } else {
            currentLoadFactor = 0.75;
        }
    }

    @Override
    final double loadFactor() {
        return currentLoadFactor;
    }
}

三、方案对比与选型建议

方案 优点 缺点 适用场景
渐进式扩容 平滑内存压力,避免长 STW 实现复杂,读写性能略有下降 高吞吐低延迟场景(如实时交易)
分片哈希 简单易实现,天然支持并发 内存碎片化,跨分片操作成本高 高并发读写的分布式中间件
动态负载因子 自适应性强,无需额外数据结构 治标不治本,极端场景仍会触发扩容 内存敏感型应用(如嵌入式系统)

推荐组合使用:分片哈希 + 渐进式扩容(参考 Redis Cluster 设计)。


四、生产级优化扩展

  1. 零拷贝迁移:使用 Unsafe 直接操作内存,避免 JVM 层面的对象复制。
  2. 非阻塞算法:采用 CAS 实现无锁迁移(类似 LongAdder 的分段思想)。
  3. 监控集成:暴露 migrationProgress 指标到 Prometheus/Grafana。
  4. 弹性容量规划:基于 Kubernetes HPA 自动调整分片数。

你可能感兴趣的:(java,开发语言)