摘要:// 看似正常的缓存实现private final ConcurrenthashMap userCache = new ConcurrentHashMap(10_000_000);public UserProfil
上个月面试阿里云,技术三面时面试官突然抛出了这个问题:"你知道为什么我们要重写HashMap吗?JDK的ConcurrentHashMap哪里不够用?"
我当时一脸懵:"啊?阿里重写了HashMap?"
面试官笑了:"看来你对我们内部的一些技术实践还不够了解。那我换个问题,你在高并发场景下用ConcurrentHashMap遇到过什么问题吗?"
这一问,让我陷入了沉思...
面试回来后,我开始仔细审视项目中ConcurrentHashMap的使用。果然发现了几个让人头疼的问题:
场景1:热点数据访问 我们有个用户画像缓存,存储千万级用户数据:
// 看似正常的缓存实现private final ConcurrenthashMap userCache = new ConcurrentHashMap(10_000_000);public UserProfile getUserProfile(String userId) {return userCache.computeIfAbsent(userId, this::loadFromDB);}问题来了:热点用户的访问会导致hash冲突严重,某些bucket的链表长度能达到上百个节点!
更要命的是内存问题。我们用JProfiler分析发现:
// 看似无害的配置更新private final ConcurrentHashMap configMap = new ConcurrentHashMap;public void updateConfig(String key, String value) {configMap.put(key, value); // 看起来很正常}问题爆发:
初始容量16,负载因子0.75随着配置增多,频繁扩容每次扩容都要重新Hash所有元素老年代碎片化严重,Full GC频繁监控数据显示:配置热更新时,GC停顿时间从50ms飙升到2秒!
带着疑问,我开始研究阿里的开源项目,发现了几个有趣的优化:
1. 分段锁的进化版
// 类似阿里内部的分段策略public class SegmentedHashMap {private final segment segments;private final int segmentMask;public V put(K key, V value) {int hash = hash(key);int segIndex = (hash >>> 28) & segmentMask;return segments[segIndex].put(key, value, hash);}// 每个segment独立扩容,避免全局锁static class Segment {private volatile HashEntry table;// ...}}2. 内存预分配策略
// 根据业务特点预估容量public class PreSizedConcurrentMap extends ConcurrentHashMap {public PreSizedConcurrentMap(int expectedSize, float loadFactor) {// 计算合适的初始容量,避免扩容super(calculateInitialCapacity(expectedSize, loadFactor), loadFactor);}private static int calculateInitialCapacity(int expected, float lf) {return (int) Math.ceil(expected / lf);}}深入研究后,我发现阿里重写HashMap主要解决这些问题:
核心痛点对比:
问题JDK ConcurrentHashMap阿里优化方案扩容成本全量rehash渐进式扩容内存开销固定Node结构紧凑型存储热点访问hash冲突严重一致性hashGC压力频繁对象创建对象池复用最关键的优化 - 渐进式扩容:
public class ProgressiveHashMap {private volatile Table oldTable;private volatile Table newTable;private final AtomicInteger migrationIndex = new AtomicInteger(0);public V get(K key) {// 先查新表,再查旧表V value = newTable.get(key);if (value == null && oldTable != null) {value = oldTable.get(key);// 顺便迁移一个bucketmigrateBucket;}return value;}private void migrateBucket {// 每次操作迁移少量数据,分摊扩容成本// ...}}这次研究让我明白,技术选型必须结合具体业务场景:
阿里的业务特点:
超大规模:单机存储亿级数据高并发:双11期间QPS百万级低延迟:99.9%请求要求高可用:不能因为扩容影响服务JDK ConcurrentHashMap的局限:
扩容时全量rehash,影响响应时间内存布局不够紧凑,浪费空间分段锁粒度不够细,高并发下仍有瓶颈受到启发,我们也对项目进行了渐进式优化:
// 优化前:简单粗暴的缓存private final Map cache = new ConcurrentHashMap;// 优化后:分层缓存 + 预分配public class OptimizedCache {private final Map hotData; // 热点数据,小容量private final Map coldData; // 冷数据,大容量public OptimizedCache(int hotSize, int coldSize) {// 根据访问模式预分配容量this.hotData = new ConcurrentHashMap(hotSize, 0.9f);this.coldData = new ConcurrentHashMap(coldSize, 0.75f);}public V get(K key) {V value = hotData.get(key);if (value != null) return value;value = coldData.get(key);if (value != null) {// 热点数据提升策略promoteToHot(key, value);}return value;}}优化效果:
P99延迟从120ms降到15ms内存使用减少30%GC停顿时间减少80%现在我明白了面试官那个问题的深意:没有完美的数据结构,只有适合业务场景的最优解。
阿里重写HashMap不是为了炫技,而是因为:
那次面试虽然没过,但让我学会了从业务视角思考技术问题。现在再有人问我类似问题,我会先反问:"你们的业务场景和性能要求是什么?"然后基于具体需求来讨论技术方案。
毕竟,脱离业务谈技术,就是耍流氓。
来源:马士兵教育