ConcurrentHashMap的“核动力引擎”详解

14 阅读4分钟

2. 并发控制的“三板斧”:CAS + synchronized + volatile

(1) CAS(Compare And Swap)——无锁的“轻功”

  • 应用场景:初始化数组、插入头节点、计数(如sizeCtl字段)等非阻塞操作。
  • 代码示例
    // 典型的CAS操作(伪代码)
    if (当前值 == 预期值) {
        更新为新值;
        return true;
    } else {
        return false;
    }
    
  • 优势:避免线程阻塞,适合低竞争场景。
  • 缺点:高竞争时可能导致大量重试(CPU空转)。

(2) synchronized锁——精准打击的“点穴术”

  • 锁对象:每个数组桶的头节点(即链表或树的根节点)。
  • 锁粒度:只锁当前操作的桶,其他桶可自由访问,彻底告别“全员排队”。
  • 优化:JDK1.6后synchronized升级为“偏向锁→轻量级锁→重量级锁”,性能媲美CAS。

举个栗子🌰:
想象一个餐厅,Hashtable是所有人排队等一个收银台,而ConcurrentHashMap是每个餐桌(桶)有自己的服务员(锁),顾客(线程)只需在目标餐桌前排队。

(3) volatile变量——实时更新的“监控探头”

  • 应用字段table数组、Node.valNode.next等。
  • 作用:保证多线程间的可见性,读取最新值,避免脏数据。

3. 扩容机制:多线程“搭把手”的高效迁移

CHM的扩容是“渐进式”的,不会一次性卡死所有线程,核心步骤:

  1. 触发条件:当元素数量超过容量 × 负载因子时触发扩容。
  2. 分配任务:每个线程处理一个“步长”(stride)的桶区间,默认16个桶。
  3. 迁移数据
    • 旧桶标记为ForwardingNode(表示正在迁移)。
    • 其他线程插入数据时,发现ForwardingNode会主动帮忙迁移。
  4. 完成扩容:所有桶迁移后,数组大小翻倍,阈值更新。

关键代码片段(简化版)

while (正在迁移) {
    if (当前线程是第一个触发扩容的线程) {
        初始化新数组(双倍大小);
    }
    分配迁移区间(从旧数组的i到i+stride);
    for (每个桶 in 分配区间) {
        if (桶未被迁移) {
            锁住桶头节点;
            迁移链表或树到新数组;
            替换为ForwardingNode;
        }
    }
}

为什么高效?

  • 并行迁移:多线程协作,分摊迁移成本。
  • 无全局锁:每个线程处理不同区间,互不干扰。

4. 哈希碰撞处理:分散与平衡的艺术

  • 哈希算法
    int hash = (key.hashCode() ^ (key.hashCode() >>> 16)) & 0x7fffffff;
    
    通过高位异或,让哈希值分布更均匀,减少碰撞概率。
  • 再哈希策略:若发生碰撞,先尝试链表插入,过长则转红黑树。

5. 源码中的“彩蛋”设计

  • sizeCtl字段:一个int值兼任多个角色:
    • 负数:表示正在初始化或扩容。
    • 正数:表示扩容阈值。
  • TreeBin类:红黑树的“管家”,内部用volatile int lockState实现读写锁,平衡树结构调整与查询的并发性。

6. 性能对比实验(附伪代码)

// 测试代码:10个线程,每个写入10000次
Map<String, Integer> map = new ConcurrentHashMap<>();
ExecutorService pool = Executors.newFixedThreadPool(10);
long start = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
    pool.submit(() -> {
        for (int j = 0; j < 10000; j++) {
            map.put(Thread.currentThread().getName() + j, j);
        }
    });
}
pool.shutdown();
pool.awaitTermination(1, TimeUnit.HOURS);
System.out.println("耗时:" + (System.currentTimeMillis() - start) + "ms");
// 结果:ConcurrentHashMap通常比Hashtable快5倍以上!

总结:ConcurrentHashMap的“内功精髓”

  • 锁粒度:从分段锁(JDK1.7)到桶锁(JDK1.8),锁粒度越来越细。
  • 数据结构:链表转红黑树,平衡查询与插入效率。
  • 并发协作:CAS + synchronized + volatile,实现无锁化与精准锁的完美结合。

最后一句金句💡:

ConcurrentHashMap的设计哲学是——“能无锁就无锁,必须锁就锁最少”。这不仅是技术的胜利,更是对“高效协作”的深刻理解!