ConcurrentHashMap

ConcurrentHashMap 是 Java 中一个用于高并发环境下的线程安全哈希表实现 ,它的设计核心是提高并发性能 ,避免像 Hashtable 那样对整个表加锁的做法。从源码角度来看,ConcurrentHashMapJava 7Java 8+ 的实现有显著差异,以 Java 8 为主。

JDK 7(分段锁 Segment)

⚠️ Java 7 使用 Segment 分段锁,每个 Segment 相当于一个小的 Hashtable,多个线程可以并发访问不同 Segment。

结构图:

java 复制代码
ConcurrentHashMap
  -> Segment[]
      -> HashEntry[]

关键点:

  • Segment 继承 ReentrantLock,每个 Segment 对应一个锁。
  • 整个哈希表被分为多个 Segment(默认 16 个),每个 Segment 管理自己的 HashEntry 链表。
  • 多线程并发时,只要访问的 Segment 不同,就不会锁冲突,提高并发性。

JDK 8+

✅ Java 8 重构了 ConcurrentHashMap去掉了 Segment 分段锁结构,改用节点级别的锁(synchronized + CAS)

核心数据结构

Node 节点

java 复制代码
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;       // volatile 保证可见性
    volatile Node<K,V> next; // volatile 保证可见性
}
  • 基础存储单元,与 HashMap 类似,但 valnextvolatile 修饰,确保多线程可见性。

特殊节点类型

  • ForwardingNode:扩容时标记迁移完成的桶(hash = -1)。
  • TreeBin:红黑树的根节点(桶结构为树时使用)。
  • ReservationNode:占位节点,用于 computeIfAbsent 等原子方法。

关键属性

java 复制代码
transient volatile Node<K,V>[] table; // 主数组
private transient volatile int sizeCtl; // 控制状态
  • sizeCtl 是关键控制变量:
    • 0:默认值为0,还未初始化
    • -1:表示正在初始化。
    • -N:表示有 N-1 个线程正在扩容。
    • 正数:下一次扩容的阈值(容量 * 负载因子)。

get --- 无锁读取

java 复制代码
public V get(Object key) {
    Node<K,V>[] tab;
    Node<K,V> e, p;
    int n, eh;
    K ek;

    int h = spread(key.hashCode()); // 扰动函数,减少哈希冲突
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) { // table被初始化,并且长度不为0,通过 (n - 1) & h 定位当前key应在的哈希桶位置
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val; // 命中头节点
        } else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null; // 特殊节点 find 查找
        while ((e = e.next) != null) { // 链表查找
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}
  • 无锁读 :依赖 volatile 保证可见性。
  • 遇到 TreeBinForwardingNodeReservationNode 时调用其 find() 方法查找。
  • tabAt(...) 是一个用 Unsafe 类操作的数组读取方法,具有原子性,保证线程安全。

扰动函数

java 复制代码
static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}
  • 相比于HashMap,ConcurrenHashMap 使用了& HASH_BITS,保证扰动函数的返回为正数

  • HashMap 是通过 &(table.length - 1) 来取索引的,不关心正负数的问题。

  • 在 ConcurrentHashMap 中,负哈希值有特殊含义

    java 复制代码
        static final int MOVED     = -1; // hash for forwarding nodes
        static final int TREEBIN   = -2; // hash for roots of trees
        static final int RESERVED  = -3; // hash for transient reservations
    哈希值 含义 节点类型
    -1 桶正在迁移 ForwardingNode
    -2 红黑树根节点 TreeBin
    -3 代表当前索引位置已经被占用,但是值还没有放进去 ReservationNode

put --- synchronized + CAS 插入

java 复制代码
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) { // 死循环,直到成功插入
        Node<K,V> f; int n, i, fh; K fk; V fv;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable(); // 初始化
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value))) // 当前桶为空,尝试cas放入新节点
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f); // hash冲突,正在扩容,协助扩容
        else if (onlyIfAbsent // check first node without acquiring lock
                 && fh == hash
                 && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                 && (fv = f.val) != null)
            return fv; // 相同的key且onlyIfAbsent,直接返回原值,不更新
        else {
            V oldVal = null;
            synchronized (f) { // 获取当前桶首节点加锁
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) { // 链表插入
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key, value);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) { // 红黑树插入
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                    else if (f instanceof ReservationNode)
                        throw new IllegalStateException("Recursive update");
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i); // 尝试树化,判断条件:桶元素>8 && 总元素>64 
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

transfer 扩容

  • 元素总数超过 sizeCtl 时触发扩容。
java 复制代码
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    // 步骤1:分配迁移任务区间(stride)
    // 步骤2:每个线程负责迁移一段桶
    while (advance) {
        // 领取一个桶的迁移任务
    }
    // 步骤3:迁移数据(链表/树拆分为高位桶和低位桶)
    synchronized (f) {
        // 迁移操作
    }
    // 步骤4:放置 ForwardingNode 标记迁移完成,在synchronized中操作 
}
  • 线程通过 transferIndex 领取迁移任务区间。每个线程最少负责16个桶的迁移工作。
  • 迁移时对桶加锁,避免并发问题。
  • 迁移完成后用 ForwardingNode 标记,读请求可转发到新表。
相关推荐
why技术12 分钟前
在我眼里,这就是天才般的算法!
后端·面试
绝无仅有13 分钟前
Jenkins+docker 微服务实现自动化部署安装和部署过程
后端·面试·github
程序视点16 分钟前
Escrcpy 3.0投屏控制软件使用教程:无线/有线连接+虚拟显示功能详解
前端·后端
hqxstudying21 分钟前
mybatis过渡到mybatis-plus过程中需要注意的地方
java·tomcat·mybatis
lichkingyang29 分钟前
最近遇到的几个JVM问题
java·jvm·算法
zhuyasen42 分钟前
当Go框架拥有“大脑”,Sponge框架集成AI开发项目,从“手写”到一键“生成”业务逻辑代码
后端·go·ai编程
ZeroKoop1 小时前
多线程文件下载 - 数组切分,截取文件名称
java
Monly211 小时前
IDEA:控制台中文乱码
java·ide·intellij-idea
叫我阿柒啊1 小时前
从全栈开发到微服务架构:一次真实的Java面试实录
java·redis·ci/cd·微服务·vue3·springboot·jwt
东皋长歌1 小时前
SpringBoot集成ELK
spring boot·后端·elk