剖析 ConcurrentHashMap 源码

ConcurrentMap接口设计分析

ConcurrentMap<K,V> 继承自 Map<K,V>,专门为并发环境设计,提供了线程安全和原子性保证的键值对存储接口。

核心抽象方法

原子性条件操作方法

  • V putIfAbsent(K key, V value) - 仅在键不存在时插入值
  • boolean remove(Object key, Object value) - 仅在键值匹配时删除
  • boolean replace(K key, V oldValue, V newValue) - 仅在旧值匹配时替换
  • V replace(K key, V value) - 仅在键存在时替换值

这些方法的设计核心是compare-and-swap (CAS) 语义,确保操作的原子性。

默认方法分析

弱一致的查询方法

getOrDefault(Object key, V defaultValue)

default V getOrDefault(Object key, V defaultValue) {
    V v;
    return ((v = get(key)) != null) ? v : defaultValue;
}

设计特点:

  • 假设实现类不支持null值
  • 通过 get() 返回null来明确表示键不存在
  • 避免了传统Map中null值的歧义性

并发安全的遍历方法

forEach(BiConsumer<? super K, ? super V> action)

default void forEach(BiConsumer<? super K, ? super V> action) {
    Objects.requireNonNull(action);
    for (Map.Entry<K,V> entry : entrySet()) {
        K k; V v;
        try {
            k = entry.getKey();
            v = entry.getValue();
        } catch (IllegalStateException ise) {
            continue; // 条目已被删除,跳过
        }
        action.accept(k, v);
    }
}

并发处理机制:

  • 捕获 IllegalStateException 来处理并发删除的条目
  • 确保遍历过程中的弱一致性语义

原子性批量更新方法

replaceAll(BiFunction<? super K, ? super V, ? extends V> function)

default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
    Objects.requireNonNull(function);
    forEach((k,v) -> {
        while (!replace(k, v, function.apply(k, v))) {
            if ((v = get(k)) == null) break; // 键已被删除
        }
    });
}

重试机制:

  • 使用 while 循环确保CAS操作成功
  • 通过 get(k) == null 检测键是否被并发删除

 条件计算方法

computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)

default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
    Objects.requireNonNull(mappingFunction);
    V oldValue, newValue;
    return ((oldValue = get(key)) == null
            && (newValue = mappingFunction.apply(key)) != null
            && (oldValue = putIfAbsent(key, newValue)) == null)
        ? newValue : oldValue;
}

原子性保证:

  • 使用 putIfAbsent() 确保只有一个线程能成功插入值
  • 短路求值避免不必要的计算

computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)

default V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
    Objects.requireNonNull(remappingFunction);
    for (V oldValue; (oldValue = get(key)) != null; ) {
        V newValue = remappingFunction.apply(key, oldValue);
        if ((newValue == null) ? remove(key, oldValue) : replace(key, oldValue, newValue))
            return newValue;
    }
    return null;
}

循环重试机制:

  • 通过 for 循环处理并发修改
  • 三元运算符根据新值是否为null选择删除或替换操作

设计原则总结

1. 无锁编程模式

所有默认方法都基于CAS操作,避免使用显式锁,提高并发性能。

2. 弱一致性语义

遍历和批量操作可能看到不一致的中间状态,但保证最终一致性。

3. Null值假设

默认实现假设不支持null值,简化了并发控制的复杂性。

4. 重试机制

通过循环和条件检查处理CAS失败,确保操作最终成功或明确失败。

这种设计使得 ConcurrentMap 成为高并发环境下键值对存储的理想选择,在保证线程安全的同时最大化了性能。

ConcurrentHashMap 深度分析

继承自 AbstractMap<K,V>,拥有Hash Map一致的能力,又 实现 ConcurrentMap<K,V>接口,表示有并发安全的能力。本节将深入分析 ConcurrentHashMap 的关键实现。

 核心数据结构

Node结构体系

// 基础节点
static class Node<K,V> implements Map.Entry<K,V>

// 树节点
static final class TreeNode<K,V> extends Node<K,V>

// 树容器
static final class TreeBin<K,V> extends Node<K,V>

// 转发节点(扩容时使用)
static final class ForwardingNode<K,V> extends Node<K,V>

// 预留节点(计算时占位)
static final class ReservationNode<K,V> extends Node<K,V>

 关键字段

transient volatile Node<K,V>[] table;           // 主表
private transient volatile Node<K,V>[] nextTable; // 扩容时的新表
private transient volatile long baseCount;      // 基础计数
private transient volatile int sizeCtl;         // 控制字段
private transient volatile int transferIndex;   // 扩容传输索引
private transient volatile CounterCell[] counterCells; // 计数单元

哈希和访问机制

哈希计算与分布

static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}

设计亮点:

  • 高16位与低16位异或,减少碰撞
  • & HASH_BITS 确保最高位为0,区分特殊节点

内存访问优化

// 获取数组元素(Acquire语义)
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    return (Node<K,V>)U.getReferenceAcquire(tab, ((long)i << ASHIFT) + ABASE);
}

// 设置数组元素(Release语义)
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
    U.putReferenceRelease(tab, ((long)i << ASHIFT) + ABASE, v);
}

内存模型保证:

  • 使用 Unsafe 直接操作内存
  • Acquire/Release 语义确保可见性
  • 避免传统 volatile 数组的性能开销

HASH_BITS

static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash

特殊hash值常量

static final int MOVED     = -1; // ForwardingNode(扩容转发节点)
static final int TREEBIN   = -2; // TreeBin(红黑树根节点)  
static final int RESERVED  = -3; // ReservationNode(占位节点)

特殊节点类型

ForwardingNode - 扩容转发节点

static final class ForwardingNode<K,V> extends Node<K,V> {
    final Node<K,V>[] nextTable;
    ForwardingNode(Node<K,V>[] tab) {
        super(MOVED, null, null); // hash = -1
        this.nextTable = tab;
    }
}

TreeBin - 红黑树容器节点

static final class TreeBin<K,V> extends Node<K,V> {
    TreeNode<K,V> root;
    volatile TreeNode<K,V> first;
    
    TreeBin(TreeNode<K,V> b) {
        super(TREEBIN, null, null); // hash = -2
        // 树结构初始化...
    }
}

ReservationNode - 预留占位节点

static final class ReservationNode<K,V> extends Node<K,V> {
    ReservationNode() {
        super(RESERVED, null, null); // hash = -3
    }
}

方法中特殊处理

get()方法中的特殊处理

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) {
        if ((eh = e.hash) == h) {
            // 普通节点:hash >= 0
        }
        else if (eh < 0) // 特殊节点:hash < 0
            return (p = e.find(h, key)) != null ? p.val : null;
        // 继续链表遍历...
    }
    return null;
}

 putVal()方法中的分支处理

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // ...
    else if ((fh = f.hash) == MOVED) // hash == -1,扩容中
        tab = helpTransfer(tab, f);
    else {
        synchronized (f) {
            if (tabAt(tab, i) == f) {
                if (fh >= 0) {
                    // 普通链表节点处理
                }
                else if (f instanceof TreeBin) { // hash == -2,红黑树
                    // 树节点插入逻辑
                }
                else if (f instanceof ReservationNode) // hash == -3
                    throw new IllegalStateException("Recursive update");
            }
        }
    }
}

transfer()扩容方法中的应用

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    // ...
    else if ((fh = f.hash) == MOVED) // 已处理的桶
        advance = true; // already processed
    else {
        synchronized (f) {
            if (tabAt(tab, i) == f) {
                if (fh >= 0) {
                    // 普通链表分裂处理
                }
                else if (f instanceof TreeBin) {
                    // 红黑树分裂处理
                }
            }
        }
    }
}

设计优势

高效的类型判断

  • 单一比较:通过 hash < 0 快速识别特殊节点
  • 减少instanceof开销:只对少数特殊节点使用instanceof
  • 分支预测友好:CPU能更好地预测分支走向

内存布局优化

  • 统一存储:所有节点类型都能存储在同一个数组中
  • 无需额外标记字段:利用hash字段的符号位作为类型标识
  • 缓存友好:减少内存访问次数

并发安全保证

  • 原子性判断:hash值读取是原子的
  • 一致性视图:通过hash值快速确定节点状态
  • 无竞态条件:避免了复杂的类型检查竞态

并发扩容机制

 扩容触发与协助

private final void addCount(long x, int check) {
    // 更新计数
    if ((cs = counterCells) != null ||
        !U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        // 分段计数逻辑
    }
    
    // 检查是否需要扩容
    if (check >= 0) {
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
            if (sc < 0) { // 已有线程在扩容
                if (/* 扩容即将完成 */) break;
                if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt); // 协助扩容
            }
            else if (U.compareAndSetInt(this, SIZECTL, sc, rs + 2))
                transfer(tab, null); // 发起扩容
        }
    }
}

并发传输算法

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int stride = (NCPU > 1) ? (n >>> 3) / NCPU : n;
    if (stride < MIN_TRANSFER_STRIDE) stride = MIN_TRANSFER_STRIDE;
    
    for (int i = 0, bound = 0;;) {
        // 认领传输区间
        while (advance) {
            if (--i >= bound || finishing) advance = false;
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1; advance = false;
            }
            else if (U.compareAndSetInt(this, TRANSFERINDEX, nextIndex,
                     nextBound = (nextIndex > stride ? nextIndex - stride : 0))) {
                bound = nextBound; i = nextIndex - 1; advance = false;
            }
        }
        
        // 处理每个桶
        synchronized (f) { // 锁定头节点
            if (tabAt(tab, i) == f) {
                // 链表分裂:根据 hash & n 分为两组
                int runBit = fh & n;
                // 优化:找到连续相同bit的尾部
                for (Node<K,V> p = f.next; p != null; p = p.next) {
                    int b = p.hash & n;
                    if (b != runBit) { runBit = b; lastRun = p; }
                }
                // 设置到新表
                setTabAt(nextTab, i, ln);
                setTabAt(nextTab, i + n, hn);
                setTabAt(tab, i, fwd); // 设置转发节点
            }
        }
    }
}

扩容特点:

  • 多线程协作:动态认领工作区间
  • 最小化锁竞争:只锁定桶的头节点
  • 零拷贝优化:连续相同bit的节点直接复用

树化与退化机制

链表转红黑树

private final void treeifyBin(Node<K,V>[] tab, int index) {
    if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
        tryPresize(n << 1); // 优先扩容而非树化
    else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
        synchronized (b) {
            if (tabAt(tab, index) == b) {
                // 构建双向链表
                TreeNode<K,V> hd = null, tl = null;
                for (Node<K,V> e = b; e != null; e = e.next) {
                    TreeNode<K,V> p = new TreeNode<K,V>(e.hash, e.key, e.val, null, null);
                    if ((p.prev = tl) == null) hd = p;
                    else tl.next = p;
                    tl = p;
                }
                setTabAt(tab, index, new TreeBin<K,V>(hd));
            }
        }
    }
}

 TreeBin读写分离

static final class TreeBin<K,V> extends Node<K,V> {
    TreeNode<K,V> root;
    volatile TreeNode<K,V> first; // 链表头,用于遍历
    volatile Thread waiter;
    volatile int lockState;
    
    final Node<K,V> find(int h, Object k) {
        if (k != null) {
            for (Node<K,V> e = first; e != null; ) {
                int s; K ek;
                if (((s = lockState) & (WAITER|WRITER)) != 0) {
                    // 有写操作,沿链表查找
                    if (e.hash == h && ((ek = e.key) == k || k.equals(ek)))
                        return e;
                    e = e.next;
                }
                else if (U.compareAndSetInt(this, LOCKSTATE, s, s + READER)) {
                    // 获取读锁,使用树查找
                    try {
                        p = ((r = root) == null ? null : r.findTreeNode(h, k, null));
                    } finally {
                        // 释放读锁
                    }
                    return p;
                }
            }
        }
        return null;
    }
}

计数机制

分段计数策略

@jdk.internal.vm.annotation.Contended 
static final class CounterCell {
    volatile long value;
}

final long sumCount() {
    CounterCell[] cs = counterCells;
    long sum = baseCount;
    if (cs != null) {
        for (CounterCell c : cs)
            if (c != null) sum += c.value;
    }
    return sum;
}

动态扩展计数单元

private final void fullAddCount(long x, boolean wasUncontended) {
    int h = ThreadLocalRandom.getProbe();
    boolean collide = false;
    for (;;) {
        CounterCell[] cs; CounterCell c; int n; long v;
        if ((cs = counterCells) != null && (n = cs.length) > 0) {
            if ((c = cs[(n - 1) & h]) == null) {
                // 创建新的计数单元
                if (cellsBusy == 0) {
                    CounterCell r = new CounterCell(x);
                    if (cellsBusy == 0 && U.compareAndSetInt(this, CELLSBUSY, 0, 1)) {
                        // 安装新单元
                    }
                }
            }
            else if (!wasUncontended) // 重新哈希
                wasUncontended = true;
            else if (U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))
                break; // 成功更新
            else if (counterCells != cs || n >= NCPU)
                collide = false;
            else if (!collide)
                collide = true;
            else if (cellsBusy == 0 && U.compareAndSetInt(this, CELLSBUSY, 0, 1)) {
                // 扩展计数表
                counterCells = Arrays.copyOf(cs, n << 1);
            }
            h = ThreadLocalRandom.advanceProbe(h);
        }
    }
}

视图实现

KeySetView特殊性

public static final class KeySetView<K,V> extends CollectionView<K,V,K>
    implements Set<K>, java.io.Serializable {
    
    private final V value; // 支持添加操作的映射值
    
    public boolean add(K e) {
        V v;
        if ((v = value) == null)
            throw new UnsupportedOperationException();
        return map.putVal(e, v, true) == null;
    }
}

弱一致性迭代器

static class Traverser<K,V> {
    Node<K,V>[] tab;
    Node<K,V> next;
    TableStack<K,V> stack, spare; // 处理转发节点
    
    final Node<K,V> advance() {
        Node<K,V> e;
        if ((e = next) != null) e = e.next;
        for (;;) {
            if (e != null) return next = e;
            if ((e = tabAt(t, i)) != null && e.hash < 0) {
                if (e instanceof ForwardingNode) {
                    tab = ((ForwardingNode<K,V>)e).nextTable;
                    pushState(t, i, n); // 保存状态
                    continue;
                }
                else if (e instanceof TreeBin)
                    e = ((TreeBin<K,V>)e).first;
            }
            // 状态恢复和索引推进逻辑
        }
    }
}

设计精髓总结

无锁优化

  • 分段锁:只锁桶头节点,最小化锁粒度
  • CAS操作:表操作、计数更新大量使用CAS
  • 内存屏障:精确控制可见性,避免过度同步

性能优化

  • 预期常数时间:链表→红黑树自适应
  • 并发扩容:多线程协作,减少停顿时间
  • 缓存友好@Contended注解避免伪共享

一致性保证

  • 弱一致性:迭代过程中允许并发修改
  • happens-before:写操作对后续读操作可见
  • 最终一致性:保证操作最终反映到所有视图

这种设计使得 ConcurrentHashMap 在高并发场景下既保证了线程安全,又实现了优异的性能,是Java并发编程的典型范例。

get(Object key) 的实现

get 方法通过分层处理(头节点→特殊节点→链表遍历)实现高效查找,结合无锁设计和原子操作,在保证线程安全的同时最大化性能。特殊节点(如扩容状态)的处理确保并发操作的健壮性,哈希计算和内存访问优化则进一步提升了效率。

    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) {
            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;
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

计算哈希值 / 定位桶

int h = spread(key.hashCode());
  • ​作用​​:计算键的哈希值,并进行扩散处理。
  • spread() 实现​​:
    static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS; // HASH_BITS = 0x7fffffff
    }
  • h ^ (h >>> 16):将哈希码的高16位与低16位异或,增加低位的随机性。
  • & HASH_BITS:确保结果为正数(最高位为0),因为负哈希值用于特殊节点(如 MOVED)。

if ((tab = table) != null && (n = tab.length) > 0 &&
    (e = tabAt(tab, (n - 1) & h)) != null) {

  1. ​检查表是否初始化​​:table 不为空且长度大于0。
  2. ​计算桶索引​​:(n - 1) & h(等价于 h % n)。
  3. ​原子获取头节点​​:tabAt() 使用 Unsafe 保证可见性:
    static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>) U.getReferenceAcquire(tab, ((long)i << ASHIFT) + ABASE);
    }
    • ASHIFTABASE:计算元素在数组中的精确内存偏移。

查找

if ((eh = e.hash) == h) {
    if ((ek = e.key) == key || (ek != null && key.equals(ek)))
        return e.val;
}
  • ​条件​​:头节点的哈希值 eh 等于计算出的哈希值 h
  • ​检查键是否相等​​:通过 ==equals() 判断。
  • ​直接返回​​:匹配则返回头节点的值 e.val
else if (eh < 0)
    return (p = e.find(h, key)) != null ? p.val : null;
  • ​场景​​:头节点的哈希值为负(特殊节点)。
  • ​调用 find()​:根据节点类型执行不同逻辑:
    • TreeBin(红黑树根节点,hash=TREEBIN=-2)​​:
      final Node<K,V> find(int h, Object k) {
          // 在红黑树中查找(比较哈希值→键→左右子树)
      }
    • ForwardingNode(扩容节点,hash=MOVED=-1)​​:
      Node<K,V> find(int h, Object k) {
          // 到新表 nextTable 中查找
          return nextTable != null ? nextTable[(nextTable.length - 1) & h].find(h, k) : null;
      }
    • ReservationNode(占位节点,hash=RESERVED=-3)​​:直接返回 null

遍历链表

while ((e = e.next) != null) {
    if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek))))
        return e.val;
}
  • ​条件​​:头节点未匹配且非特殊节点(普通链表)。
  • ​遍历链表​​:依次检查每个节点的哈希值和键。
  • ​返回匹配值​​:找到匹配节点则返回 e.val

未找到返回 null

关键设计要点

  1. ​无锁读操作​​:

    • 全程无同步(如 synchronized),依赖 volatile 变量和 Unsafe 原子操作保证可见性。
    • tabAt() 使用 getReferenceAcquire 确保读取最新数据。
  2. ​处理并发扩容​​:

    • 遇到 ForwardingNode 时,通过 find() 自动重定向到新表 nextTable
    • 扩容期间读操作无需阻塞。
  3. ​高效处理树化​​:红黑树(TreeBin)的 find() 时间复杂度为 O(log n),且通过读写锁分离保证并发安全。

  4. ​哈希值优化​​:

    • spread() 确保哈希值为正数,避免与特殊节点冲突。
    • 桶索引计算 (n-1)&h 高效且分布均匀。

putVal() 实现

参数验证与初始化

if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
  • ​空值检查​​:禁止 null 键值(区别于 HashMap)
  • ​哈希计算​​:spread() 处理哈希值(同 get 方法)
  • ​binCount​​:记录桶中节点数(用于树化判断)

主循环(CAS + 同步块)

for (Node<K,V>[] tab = table;;) {
    Node<K,V> f; int n, i, fh; K fk; V fv;
    // 分支处理...
}

自旋保证操作成功,处理五种情况:

分支1: 表未初始化

if (tab == null || (n = tab.length) == 0)
    tab = initTable();
  • initTable()​:
    while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {
            // 创建数组,设置 sizeCtl = 0.75 * n
        }
    }
    • CAS 设置 sizeCtl 为 -1(初始化锁)
    • 创建默认容量 16 的数组
    • 设置扩容阈值 sizeCtl = 12 (n - n/4)

分支2: 空桶插入(无锁 CAS)

else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
        break;
}
  • ​原子操作​​:casTabAt() 使用 CAS 创建新节点
    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSetReference(tab, address, c, v);
    }
  • ​成功条件​​:桶位仍为空(未被其他线程修改)

分支3: 检测到扩容(协助迁移)

else if ((fh = f.hash) == MOVED)
    tab = helpTransfer(tab, f);
  • ​MOVED 标识​​:头节点为 ForwardingNode(hash=-1)
  • helpTransfer()​:
    • 检查扩容状态(nextTable 非空)
    • 调用 transfer() 协助数据迁移
    • 完成后返回新表继续操作

分支4: putIfAbsent 快速路径

else if (onlyIfAbsent && fh == hash &&
         ((fk = f.key) == key || (fk != null && key.equals(fk))) &&
         (fv = f.val) != null)
    return fv;
  • ​快速失败​​:仅当 putIfAbsent=true 时触发
  • ​无锁检查​​:不获取锁直接检查头节点
  • ​条件满足​​:键匹配且值非空 → 返回现有值

分支5: 哈希冲突处理(同步块)

synchronized (f) {
    if (tabAt(tab, i) == f) {
        // 链表或树处理...
    }
}
5.1 链表操作
if (fh >= 0) {
    binCount = 1;
    for (Node<K,V> e = f;; ++binCount) {
        // 1. 键已存在:更新值(onlyIfAbsent=false)
        if (e.hash == hash && ((ek = e.key) == key || ...)) {
            oldVal = e.val;
            if (!onlyIfAbsent) e.val = value;
            break;
        }
        // 2. 键不存在:链表尾部插入
        Node<K,V> pred = e;
        if ((e = e.next) == null) {
            pred.next = new Node<>(hash, key, value);
            break;
        }
    }
}
  • ​遍历链表​​:检查键是否存在
  • ​更新或插入​​:根据 onlyIfAbsent 决定是否更新
  • ​binCount 计数​​:记录链表长度
5.2 红黑树操作
else if (f instanceof TreeBin) {
    binCount = 2;
    TreeBin<K,V> t = (TreeBin<K,V>)f;
    TreeNode<K,V> p = t.putTreeVal(hash, key, value);
    if (p != null) { // 键已存在
        oldVal = p.val;
        if (!onlyIfAbsent) p.val = value;
    }
}
  • putTreeVal()​:
    • 查找键是否存在(O(log n))
    • 不存在时插入新节点并平衡红黑树
  • ​binCount=2​​:树节点统计方式不同
5.3 状态验证
if (binCount != 0) {
    if (binCount >= TREEIFY_THRESHOLD)
        treeifyBin(tab, i);
    if (oldVal != null) return oldVal;
    break;
}
  • ​树化检查​​:链表长度 ≥8 时 treeifyBin()
    • 实际树化条件:数组长度 ≥64,否则扩容
  • ​完成条件​​:找到旧值或成功插入时跳出循环

计数与扩容检查

addCount(1L, binCount);
return null;
  • addCount()​:
    CounterCell[] cs; long b, s;
    if ((cs = counterCells) != null || !U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        // 使用 CounterCell 分片计数
    }
    if (check >= 0) { // 检查扩容
        while (s >= (long)(sc = sizeCtl) && ...) {
            transfer(); // 触发扩容
        }
    }
  • ​计数机制​​:
    • 优先尝试 CAS 更新 baseCount
    • 竞争激烈时使用 CounterCell[] 分片计数
  • ​扩容触发​​:元素数 ≥ sizeCtl 时启动扩容

关键设计亮点

  1. ​分层锁策略​​:

    • 空桶:无锁 CAS
    • 非空桶:synchronized 锁头节点(粒度小)
    • 扩容:多线程协作
  2. ​锁降级检测​​:

    synchronized (f) {
        if (tabAt(tab, i) == f) { // 二次验证

    防止加锁过程中桶被修改(如树化/扩容)

  3. ​高效扩容​​:

    • 迁移时旧桶锁住,不影响新桶操作
    • ForwardingNode 机制允许读操作无等待
  4. ​计数优化​​:

    • 分片计数 (CounterCell) 避免 CAS 竞争
    • 延迟更新:优先更新 baseCount,竞争时转分片
  5. ​树化策略​​:

    • 链表长度 ≥8 且数组长度 ≥64 才树化
    • 否则优先扩容(避免小表树化开销)

总结

putVal 方法通过分层锁策略(CAS + synchronized)和状态检测,实现高并发下的安全操作。其核心创新点在于:

  1. 空桶无锁 CAS 最大化并发
  2. 非空桶锁分离(每个桶独立)
  3. 扩容协作机制
  4. 智能树化策略

配合分片计数和精确的内存操作,在保证线程安全的前提下,性能接近无锁 HashMap。

sizeCtl 机制深度解析

sizeCtl 是 ConcurrentHashMap 中最重要的控制变量,它是一个 volatile int 类型字段,负责协调表初始化、扩容和线程协作。其值在不同阶段有不同含义:

值范围含义
-1表正在初始化
<-1 (负数)

低16位标识 正在扩容的线程数+1;

高16位:Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1))

Integer.numberOfLeadingZeros(n) 根据表长度 n【表长度每次扩容都会增加,因此唯一】 生成唯一指纹;不同长度的表有不同的前导零数量,防止不同扩容周期相互干扰

0默认状态,表示初始容量为 16
>0下次扩容的阈值(当前容量 * 负载因子)或初始容量

初始化表 (initTable())​​:

while ((tab = table) == null || tab.length == 0) {
    if ((sc = sizeCtl) < 0) Thread.yield(); // 其他线程正在初始化
    else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) { // CAS 抢锁
        // 初始化操作...
        sizeCtl = n - (n >>> 2); // 设置阈值 = 0.75*n
    }
}

扩容标记机制详解

1. 扩容标记生成

// 在 addCount() 中
int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;

resizeStamp() 方法生成唯一的扩容标记:

static final int resizeStamp(int n) {
    return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
  • ​Integer.numberOfLeadingZeros(n)​​:计算 n 的二进制前导零个数
    • 例如 n=16 (000...00010000),前导零=27=0x1B
  • ​位或操作​​:| (1 << (RESIZE_STAMP_BITS - 1))
    • RESIZE_STAMP_BITS = 16
    • 1 << 15 = 0x8000
  • 低16位最高位为1(保证之后左移16位变为负数),剩余15位包含前导零信息

示例:n=16 时,sizeCtl = (0x801B << 16) + 2 = 0x801B0000 + 2 = 0x801B0002

扩容设置

// 在 addCount() 中
else if (U.compareAndSetInt(this, SIZECTL, sc, rs + 2))
    transfer(tab, null);
  • 计算​​:
    • +2:实际表示 1 + 初始线程数
      • 低16位表示 (扩容线程数 + 1)
      • 初始值2 表示有1个线程在扩容(2 = 1 + 1)

协助扩容设置

// 在 helpTransfer() 中
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {
    transfer(tab, nextTab);
}
  • ​sc + 1​​:增加扩容线程计数
  • 低16位增加:从 0x801B0002 → 0x801B0003(表示2个线程)

扩容线程退出机制

// 在 transfer() 中
if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
        return;
    // 最后一个线程的收尾工作...
}

步骤解析:

  1. ​减少线程计数​​:sc - 1(低16位减1)
  2. ​检查是否最后一个线程​​:
    (sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT
    • sc - 2:移除初始线程计数
    • resizeStamp(n) << RESIZE_STAMP_SHIFT:纯扩容标记(高16位)
    • 比较:如果不等,说明还有其他线程在工作

状态变化示例:

阶段sizeCtl 值二进制表示含义
初始0x801B000210000000 00011011 00000000 000000101个线程扩容
加入0x801B000310000000 00011011 00000000 000000112个线程扩容
退出0x801B000210000000 00011011 00000000 000000101个线程剩余
最后0x801B000110000000 00011011 00000000 00000001比较:
0x801B0001 - 2 = 0x801BFFFF
!= 0x801B0000 → 非最后线程

首次扩容保证机制

如何保证第一个 transfer?只有传入null才会初始化

// 在 addCount() 中
else if (U.compareAndSetInt(this, SIZECTL, sc, 
                            rs + 2))
    transfer(tab, null); // 第一个线程执行迁移
  1. ​CAS 操作​​:原子性设置 sizeCtl
  2. ​负值保证​​:rs + 2 一定是负数(最高位=1)
  3. ​状态隔离​​:
    • 正数:正常状态
    • -1:初始化中
    • 其他负数:扩容中

扩容状态检测:

// 在 addCount() 中
if (sc < 0) { // 已有扩容进行中
    // 检查扩容是否兼容
    if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
                (nt = nextTable) == null || transferIndex <= 0)
        break;
    // 加入扩容
    if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
        transfer(tab, nt);
}

当以下任一条件满足时,当前线程​​不参与​​扩容:

if (
    扩容线程数已达上限(65534) ||  // sc == rs + MAX_RESIZERS
    扩容已结束              ||  // sc == rs + 1
    扩容已完成             ||  // nextTable == null
    无待迁移桶              ||  // transferIndex <= 0
) {
    break; // 不参与扩容
}

 sc == rs + MAX_RESIZERS

  • sc​:当前 sizeCtl 值
  • rs​:resizeStamp(n) << RESIZE_STAMP_SHIFT(扩容标记)
  • MAX_RESIZERS​:最大扩容线程数
    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
    // RESIZE_STAMP_BITS=16 → MAX_RESIZERS=65535

​含义​​:
检查扩容线程数是否已达上限。当 sc - rs = MAX_RESIZERS 时,表示:

sc = rs + MAX_RESIZERS
  = (扩容标记) + 65535

由于 sizeCtl 的低16位表示 (扩容线程数 + 1),此条件等价于:

(扩容线程数 + 1) = 65535
即:当前已有 65534 个线程在扩容

sc == rs + 1

  • rs + 1​:(resizeStamp(n) << RESIZE_STAMP_SHIFT) + 1

​含义​​:
检查扩容是否处于结束状态。在扩容过程中:

  • 初始状态:sc = rs + 2(1个线程)
  • 线程退出时:sc = sc - 1
  • sc == rs + 1 时:
    rs + 1 = (rs + 2) - 1
    表示所有线程都已退出

(nt = nextTable) == null

​含义​​:
检查扩容是否已完成。nextTable 是扩容中的临时新表:

  • 非空:扩容进行中
  • null:扩容已完成或未开始

transferIndex <= 0

​含义​​:
检查是否还有待迁移的桶。transferIndex 表示下一个待分配迁移任务的起始索引:

  • 0:还有待迁移桶

  • ≤0:所有桶已分配完毕

这个条件判断是 ConcurrentHashMap 扩容机制的​​智能调度中枢​​,它通过精妙的位运算和状态编码实现了:

  1. ​线程数控制​​:防止过多线程竞争
  2. ​扩容状态检测​​:识别开始/结束状态
  3. ​任务分配​​:根据剩余工作量决策
  4. ​资源管理​​:避免无效操作

设计原理总结

  1. ​复合状态编码​​:

    • 高16位:扩容标记(表长度指纹)
    • 低16位:扩容线程数 + 1
  2. ​无锁协作​​:

    • CAS 更新线程计数
    • 避免全局锁竞争
  3. ​状态机转换​​:这里rs表示resizeStamp(n),更合理

​扩容兼容检查​​:

if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
                        (nt = nextTable) == null || transferIndex <= 0)
  • 确保协助线程扩容的是同一张表
  • 防止不同扩容周期混淆

这种精妙的设计使得 ConcurrentHashMap 能在高并发环境下,高效协调多线程完成扩容任务,同时保证数据一致性和操作原子性,是 Java 并发编程的经典范例。

helpTransfer() 方法详解

方法定义与作用

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    // 方法实现...
}
  • ​核心作用​​:协助进行中的扩容操作
  • ​触发时机​​:当线程执行操作(如 put/remove)时发现桶已被标记为 ForwardingNode
  • ​设计目标​​:让多个线程协同完成数据迁移,加速扩容过程

前置条件检查

if (tab != null && (f instanceof ForwardingNode) &&
    (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
  • ​三个必要条件​​:
    1. 当前表非空 (tab != null)
    2. 桶节点是 ForwardingNode 类型
    3. 新表 nextTable 已创建(扩容进行中)

​ForwardingNode 关键属性​​:

static final class ForwardingNode<K,V> extends Node<K,V> {
    final Node<K,V>[] nextTable; // 指向新表的引用
    // ...
}

扩容状态准备

int rs = resizeStamp(tab.length) << RESIZE_STAMP_SHIFT;
  • ​计算扩容标记​​:
    • resizeStamp(tab.length):生成表长度的唯一标识
    • 左移16位:将标记移到高16位

 协作循环

while (nextTab == nextTable && table == tab &&
       (sc = sizeCtl) < 0) {
    // 退出条件检查...
    // 尝试加入扩容...
}
  • ​循环条件​​:
    • 新表未改变 (nextTab == nextTable)
    • 主表未改变 (table == tab)
    • 仍处于扩容状态 (sizeCtl < 0)

退出条件判断

if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
    transferIndex <= 0)
    break;
  • ​三种退出情况​​:
    1. sc == rs + MAX_RESIZERS:扩容线程数已达上限(65535)
    2. sc == rs + 1:扩容已结束(所有线程退出)
    3. transferIndex <= 0:无待迁移桶

加入扩容

if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {
    transfer(tab, nextTab);
    break;
}
  • ​CAS 操作​​:增加扩容线程计数 (sc + 1)
  • ​执行迁移​​:调用 transfer() 参与数据迁移
  • ​退出循环​​:任务分配成功

何时触发 transfer()

首次触发(扩容发起)

// addCount() 方法中
else if (U.compareAndSetInt(this, SIZECTL, sc, 
                           rs + 2))
    transfer(tab, null); // 第一个线程执行迁移
  • ​触发条件​​:元素数量超过阈值 (sizeCtl)
  • ​执行线程​​:检测到扩容需求的线程
  • ​关键动作​​:
    • 创建新数组(2倍容量)
    • 设置 transferIndex = 旧表长度
    • 初始化迁移状态

协助触发(协作扩容)

// helpTransfer() 方法中
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {
    transfer(tab, nextTab); // 协助线程执行迁移
}
  • ​触发条件​​:操作时遇到 ForwardingNode
  • ​执行线程​​:任意执行 put/remove 等操作的线程
  • ​关键动作​​:
    • 增加扩容线程计数
    • 参与数据迁移任务

重试触发(迁移过程中)

// transfer() 方法内部
if (i < 0 || i >= n || i + n >= nextn) {
    if (finishing) { /*...*/ }
    else if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
        // 最后一个线程重新扫描
    }
}
  • ​触发条件​​:迁移任务完成或需要重新分配
  • ​执行线程​​:已完成分配任务的迁移线程
  • ​关键动作​​:
    • 减少线程计数
    • 最后一个线程执行收尾工作

    transfer() 方法深度解析

    初始化迁移参数

    int n = tab.length, stride;
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    • ​目的​​:计算每个线程处理的桶区间(步长)
    • ​计算逻辑​​:
      • 多核CPU:(数组长度/8) / CPU核心数
      • 单核CPU:处理整个数组
    • ​最小值限制​​:MIN_TRANSFER_STRIDE = 16,避免任务划分过细
    • ​设计意图​​:平衡负载,确保每个线程处理足够多的桶

     初始化新数组(仅由首个扩容线程执行)

    if (nextTab == null) {            // initiating
        try {
            @SuppressWarnings("unchecked")
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; // 双倍扩容
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE; // 处理OOME,禁止后续扩容
            return;
        }
        nextTable = nextTab;          // 设置全局新表引用
        transferIndex = n;            // 迁移起始索引(从后向前)
    }
    • ​执行条件​​:nextTab == null(首个扩容线程)
    • ​关键操作​​:
      • 创建双倍大小的新数组
      • 设置 transferIndex = n(从数组末尾开始迁移)
      • OOME 时设置 sizeCtl=Integer.MAX_VALUE 禁止扩容

    准备迁移状态

    int nextn = nextTab.length;
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    boolean advance = true;           // 推进标志
    boolean finishing = false;        // 完成标志
    • ​ForwardingNode​​:特殊节点(hash=MOVED),标记已迁移桶
    • ​状态标志​​:
      • advance:控制是否处理下一个桶
      • finishing:标记迁移进入收尾阶段

    迁移任务分配(核心循环)

    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        // 任务分配循环
        while (advance) {
            int nextIndex, nextBound;
            if (--i >= bound || finishing)       // 当前区间未完成
                advance = false;
            else if ((nextIndex = transferIndex) <= 0) { // 所有桶已分配
                i = -1;
                advance = false;
            }
            // CAS 分配迁移区间 [nextBound, nextIndex-1]
            else if (U.compareAndSetInt(this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ? nextIndex - stride : 0))) {
                bound = nextBound;    // 当前线程负责区间的下界
                i = nextIndex - 1;    // 从后向前处理
                advance = false;
            }
        }
        // 迁移状态检查...
    }
    • ​区间分配​​:通过 TRANSFERINDEX 的 CAS 操作分配任务区间
    • ​迁移方向​​:从数组末尾向前迁移(i = nextIndex - 1
    • ​设计优势​​:避免头部热点竞争,提高并发效率

    迁移完成检测

    if (i < 0 || i >= n || i + n >= nextn) {
        int sc;
        if (finishing) {              // 最终完成
            nextTable = null;         // 清除临时引用
            table = nextTab;          // 更新主表引用
            sizeCtl = (n << 1) - (n >>> 1); // 0.75 * 2n
            return;
        }
        // 当前线程完成工作,减少扩容线程计数
        if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
            // 检查是否是最后一个扩容线程
            if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                return;
            finishing = advance = true; // 进入最终检查
            i = n; // 重新扫描整个表
        }
    }
    • ​完成条件​​:
      • finishing=true:设置新表,更新阈值(sizeCtl = 1.5n
      • 非最后一个线程:直接退出
    • ​扩容线程计数​​:
      • 扩容开始时:sizeCtl = (rs << RESIZE_STAMP_SHIFT) + 2
      • 每个线程加入:sizeCtl + 1
      • 每个线程退出:sizeCtl - 1
    • ​最终检查​​:最后一个线程重新扫描全表

    空桶/已迁移 处理

    else if ((f = tabAt(tab, i)) == null)
        advance = casTabAt(tab, i, null, fwd); // 标记为已迁移
    • ​操作​​:CAS 设置 ForwardingNode
    • ​目的​​:标记桶已迁移,其他线程可跳过

    已迁移桶处理

    else if ((fh = f.hash) == MOVED)
        advance = true; // 已处理,直接推进
    • ​快速跳过​​:遇到 ForwardingNode 直接处理下一个桶

    链表迁移(核心)

    synchronized (f) {
        if (tabAt(tab, i) == f) { // 双重检查
            if (fh >= 0) {
                // 1. 计算最后一段连续相同位的节点
                int runBit = fh & n;
                Node<K,V> lastRun = f;
                for (Node<K,V> p = f.next; p != null; p = p.next) {
                    int b = p.hash & n;
                    if (b != runBit) {
                        runBit = b;
                        lastRun = p;
                    }
                }
                
                // 2. 创建高低位链表头
                if (runBit == 0) {
                    ln = lastRun;
                    hn = null;
                } else {
                    hn = lastRun;
                    ln = null;
                }
                
                // 3. 构建高低位链表
                for (Node<K,V> p = f; p != lastRun; p = p.next) {
                    int ph = p.hash; K pk = p.key; V pv = p.val;
                    if ((ph & n) == 0)
                        ln = new Node<K,V>(ph, pk, pv, ln);
                    else
                        hn = new Node<K,V>(ph, pk, pv, hn);
                }
                
                // 4. 设置新表位置
                setTabAt(nextTab, i, ln);       // 原位
                setTabAt(nextTab, i + n, hn);   // 偏移n位
                setTabAt(tab, i, fwd);          // 设置转发节点
                advance = true;
            }
            // 树节点处理...
        }
    }
    • ​优化技巧​​:
      1. ​最后连续段优化​​:直接复用最后一段相同位的节点链
      2. ​头插法构建链表​​:避免尾部遍历,O(1) 插入
      3. ​位运算分桶​​:(ph & n) == 0 判断高低位
    • ​位置计算​​:
      • 低位桶:保持原位 i
      • 高位桶:i + n(原数组长度)

    树节点迁移

    else if (f instanceof TreeBin) {
        TreeBin<K,V> t = (TreeBin<K,V>)f;
        TreeNode<K,V> lo = null, loTail = null;
        TreeNode<K,V> hi = null, hiTail = null;
        int lc = 0, hc = 0;
        
        // 1. 遍历树节点
        for (Node<K,V> e = t.first; e != null; e = e.next) {
            int h = e.hash;
            TreeNode<K,V> p = new TreeNode<K,V>(h, e.key, e.val, null, null);
            if ((h & n) == 0) {  // 低位
                if ((p.prev = loTail) == null)
                    lo = p;
                else
                    loTail.next = p;
                loTail = p;
                ++lc;
            } else {            // 高位
                if ((p.prev = hiTail) == null)
                    hi = p;
                else
                    hiTail.next = p;
                hiTail = p;
                ++hc;
            }
        }
        
        // 2. 判断是否需要树化
        ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
            (hc != 0) ? new TreeBin<K,V>(lo) : t;
        hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
            (lc != 0) ? new TreeBin<K,V>(hi) : t;
        
        // 3. 设置新表
        setTabAt(nextTab, i, ln);
        setTabAt(nextTab, i + n, hn);
        setTabAt(tab, i, fwd);
        advance = true;
    }
    • ​树迁移特点​​:
      1. 拆分为两个链表(低位/高位)
      2. 根据阈值决定树化或链表化:
        • UNTREEIFY_THRESHOLD=6:≤6时转为链表
        • MIN_TREEIFY_CAPACITY=64:小表不树化
      3. 重用 TreeBin 对象(当另一子树为空时)

    设计亮点总结

    1. ​并行迁移算法​​:

      • 任务划分:stride 控制任务粒度
      • 区间分配:TRANSFERINDEX CAS 分配
      • 方向优化:从后向前避免竞争
    2. ​无锁化设计​​:

      • 空桶:CAS 设置 ForwardingNode
      • 已迁移桶:直接跳过
      • 冲突桶:synchronized 细粒度锁
    3. ​迁移优化技巧​​:

      • 链表:最后连续段优化 + 头插法
      • 树:节点复用 + 动态树化/链表化
      • 位运算:高效计算新位置 (ph & n)
    4. ​状态机设计​​:

      • advance:控制处理流程
      • finishing:标记最终状态
      • sizeCtl:协调线程退出
    5. ​健壮性保障​​:

      • 双重检查锁:tabAt(tab, i) == f
      • OOME 处理:禁止后续扩容
      • 并发控制:CAS + synchronized 协同

    总结

    transfer() 方法是 ConcurrentHashMap 高并发的核心,其设计亮点在于:

    1. 多线程协同迁移的无锁化任务分配
    2. 精细控制的迁移状态机
    3. 针对链表和树的不同优化策略
    4. 全程保证并发访问的可用性

    通过 CAS 操作协调多线程,结合细粒度锁和智能的迁移算法,在保证线程安全的同时,实现了高效的并发扩容能力,这也是 ConcurrentHashMap 能支撑高并发场景的关键所在。

    computeIfAbsent 

    首先,我们来看一下这个方法的核心功能和契约:

    • 作用:如果指定的 key 不存在或对应的值为 null,则尝试使用给定的 mappingFunction 计算其值,并将其存入 Map 中(除非计算结果为 null)。
    • 返回值:返回与 key 关联的当前值(已存在的或新计算的)。如果计算结果为 null,则返回 null 且不会在 Map 中添加任何映射。
    • 核心原子性保证:整个方法的调用是原子性的。对于一个给定的 key,如果它不存在,mappingFunction 最多只会被调用一次。即使多个线程同时为同一个不存在的 key 调用 computeIfAbsent,也只有一个线程会执行 mappingFunction,其他线程会等待计算结果。

    这个原子性保证是此方法与 if (!map.containsKey(key)) { map.put(key, value); } 这种“先检查后执行”的非原子操作的根本区别。后者在并发环境下存在竞态条件,可能导致 mappingFunction 被多次调用或数据不一致。

    下面我们深入代码,看看 ConcurrentHashMap 是如何实现上述保证的。

    // ... existing code ...
        public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
            if (key == null || mappingFunction == null)
                throw new NullPointerException();
            int h = spread(key.hashCode());
            V val = null;
            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) & h)) == null) {
                    Node<K,V> r = new ReservationNode<K,V>();
                    synchronized (r) {
                        if (casTabAt(tab, i, null, r)) {
                            binCount = 1;
                            Node<K,V> node = null;
                            try {
                                if ((val = mappingFunction.apply(key)) != null)
                                    node = new Node<K,V>(h, key, val);
                            } finally {
                                setTabAt(tab, i, node);
                            }
                        }
                    }
                    if (binCount != 0)
                        break;
                }
                else if ((fh = f.hash) == MOVED)
                    tab = helpTransfer(tab, f);
                else if (fh == h    // check first node without acquiring lock
                         && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                         && (fv = f.val) != null)
                    return fv;
                else {
                    boolean added = false;
                    synchronized (f) {
                        if (tabAt(tab, i) == f) {
                            if (fh >= 0) {
                                binCount = 1;
                                for (Node<K,V> e = f;; ++binCount) {
                                    K ek;
                                    if (e.hash == h &&
                                        ((ek = e.key) == key ||
                                         (ek != null && key.equals(ek)))) {
                                        val = e.val;
                                        break;
                                    }
                                    Node<K,V> pred = e;
                                    if ((e = e.next) == null) {
                                        if ((val = mappingFunction.apply(key)) != null) {
                                            if (pred.next != null)
                                                throw new IllegalStateException("Recursive update");
                                            added = true;
                                            pred.next = new Node<K,V>(h, key, val);
                                        }
                                        break;
                                    }
                                }
                            }
                            else if (f instanceof TreeBin) {
    // ... (TreeBin logic is similar but for red-black trees) ...
    // ... existing code ...
                            }
                            else if (f instanceof ReservationNode)
                                throw new IllegalStateException("Recursive update");
                        }
                    }
                    if (binCount != 0) {
                        if (binCount >= TREEIFY_THRESHOLD)
                            treeifyBin(tab, i);
                        if (!added)
                            return val;
                        break;
                    }
                }
            }
            if (val != null)
                addCount(1L, binCount);
            return val;
        }
    // ... existing code ...
    

    阶段一:初始化与无限循环

    1. if (key == null || mappingFunction == null):首先进行空指针检查,这是 Map 接口的基本要求。
    2. int h = spread(key.hashCode()):计算 key 的哈希值。spread 函数通过一些位运算让哈希值的高位和低位都参与运算,目的是为了减少哈希冲突,让元素在哈希表中分布得更均匀。
    3. for (Node<K,V>[] tab = table;;):这是一个无限循环。ConcurrentHashMap 的很多写操作都采用这种模式。如果在循环体内部的某次尝试因为并发冲突(比如 CAS 失败)而失败,循环会继续,直到操作成功为止。

    阶段二:处理不同情况

    在循环内部,代码根据哈希桶(bin)的当前状态,分情况处理:

    • 情况 A:哈希表未初始化

      if (tab == null || (n = tab.length) == 0)
          tab = initTable();
      

      如果内部数组 table 还是 null,当前线程会调用 initTable() 来进行线程安全的初始化。

    • 情况 B:目标哈希桶为空 (关键的原子性实现)

      else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {
          Node<K,V> r = new ReservationNode<K,V>();
          synchronized (r) {
              if (casTabAt(tab, i, null, r)) {
                  // ... call mappingFunction and set node ...
              }
          }
          // ...
      }
      

      这是最巧妙的部分之一。如果计算出的哈希桶索引 i 处是空的:

      1. 创建占位符:创建一个临时的 ReservationNode。这是一个特殊的节点,仅用作占位和锁对象。
      2. 细粒度锁synchronized (r) 对这个临时的占位符节点加锁。这是一个非常细粒度的锁,只影响当前这个哈希桶的初始化操作。
      3. CAS 占位:在同步块内部,使用 casTabAt (Compare-And-Swap) 尝试将 ReservationNode 放入空桶中。
      4. 执行计算:如果 CAS 成功,说明当前线程成功“预定”了这个桶。此时,它就可以安全地调用 mappingFunction.apply(key) 来计算值。因为持有锁,并且成功放置了占位符,其他线程无法对这个桶做任何操作。
      5. 设置结果finally 块确保无论计算结果如何(成功、null 或异常),都会用 setTabAt 将最终的节点(新节点或 null)设置回哈希桶,替换掉 ReservationNode

      原子性如何保证? 如果两个线程同时发现桶为空,它们都会创建自己的 ReservationNode。但只有一个线程的 casTabAt 会成功。失败的那个线程会重新进入 for 循环,再次读取时会发现桶里已经不是 null 了(可能是 ReservationNode 或其他节点),然后会进入其他逻辑分支。

    对一个刚刚创建的、其他线程都看不见的局部变量 r 加锁,第一眼看上去确实像是多此一举。

    然而,这正是实现 computeIfAbsent 原子性的关键所在。这里的 synchronized 并非无用,而是与 CAS (Compare-And-Swap) 操作结合,实现了一种高效的、细粒度的“占位锁定”机制。

    我们来分解一下这个过程:

    1. 创建局部“锁票”: 当一个线程(比如线程 A)发现某个哈希桶(bucket)为空时,它会创建一个 ReservationNode 实例 r。此时,r 确实是线程 A 的一个局部变量,就像一张它自己打印的“门票”。

    2. 尝试原子性地“贴上门票”: 关键的一步是 casTabAt(tab, i, null, r)。这是一个原子操作,它会检查哈希桶 i 是否为 null,如果是,就将线程 A 的“门票” r 放入这个桶中。

      • 由于 CAS 的原子性,对于同一个空桶,只有一个线程能成功完成这个操作。
      • 一旦线程 A 成功,这个原本是局部的 r 对象,现在就被发布到了所有线程共享的 table 数组中。它从一张“私有门票”变成了一张贴在门上的“已占座”的公开告示
    3. 持有锁并执行计算: synchronized (r) 块在 casTabAt 操作之后仍然有效。这意味着成功贴上“门票”的线程 A,在执行 mappingFunction 进行耗时计算时,仍然持有 r 对象的锁。

    4. 其他线程的等待

      • 如果另一个线程(线程 B)在线程 A 之后也来访问这个桶,它会发现桶里不再是 null,而是一个 ReservationNode (也就是线程 A 放入的 r)。
      • 线程 B 会进入 else 分支,并尝试执行 synchronized (f),这里的 f 正是线程 A 放入的那个 r 对象。
      • 由于线程 A 正在计算并且还未释放 r 的锁,线程 B 就会在这里阻塞等待。它不会去执行自己的计算,而是等待线程 A 计算出结果。
    5. 完成并唤醒: 线程 A 计算完毕后,会将结果放入桶中(替换掉 ReservationNode),然后退出 synchronized 块,释放锁。此时,等待的线程 B 被唤醒,它可以继续执行并最终拿到线程 A 的计算结果。

    这个 synchronized (r) 的目的不是为了锁住一个局部变量,而是为了锁住一个被原子地发布到共享内存中的占位符。它巧妙地将一个局部对象提升为了一个针对单个哈希桶的临时锁,从而保证了在并发场景下,对于同一个不存在的 key,昂贵的计算函数 mappingFunction 只会被执行一次。

    • 情况 C:哈希表正在扩容

      else if ((fh = f.hash) == MOVED)
          tab = helpTransfer(tab, f);
      

      如果桶的头节点哈希值为 MOVED,说明哈希表正在进行扩容。当前线程不会等待扩容完成,而是会调用 helpTransfer "帮助"扩容,然后在新表上重试操作。这是一种协作式的、无锁的扩容机制。

    • 情况 D:键已存在(快速路径)

      else if (fh == h && ((fk = f.key) == key || ...))
          return fv;
      

      这是一个无锁的快速路径优化。如果桶的第一个节点正好就是我们要找的 key,并且它的 val 不为 null,就直接返回,避免了任何加锁开销。

    • 情况 E:哈希桶非空(通用路径)

      else {
          synchronized (f) {
              if (tabAt(tab, i) == f) { // Double-check
                  // ... traverse list or tree ...
              }
          }
      }
      

      如果桶不为空,且不是以上特殊情况,就进入这个通用处理逻辑:

      1. 锁住头节点synchronized (f),这里的 f 是桶的头节点。这会锁住整个哈希桶(无论是链表还是红黑树),防止其他线程修改它。
      2. 双重检查if (tabAt(tab, i) == f) 是一种双重检查锁定模式。确保从读到加锁的这段时间里,头节点没有被其他线程改变。
      3. 遍历查找:在同步块内部,安全地遍历链表或红黑树。
        • 如果找到 key,就直接获取 val 并跳出。
        • 如果遍历到末尾都没找到,就调用 mappingFunction.apply(key) 计算新值,并创建一个新节点追加到链表或树的末尾。

      原子性如何保证? mappingFunction 的调用和新节点的插入都发生在 synchronized (f) 块内部。因此,对于同一个哈希桶,一次只有一个线程能执行这段代码。如果两个线程为同一个桶里的同一个不存在的 key 调用 computeIfAbsent,它们都会尝试获取 f 的锁。获得锁的线程会执行计算和插入,释放锁后,另一个线程获得锁,再次遍历时就会发现 key 已经存在,于是直接返回值,不会再调用 mappingFunction

    阶段三:收尾工作

    1. treeifyBin(tab, i):如果在一个桶中添加节点后,发现链表长度超过了阈值 TREEIFY_THRESHOLD(默认为8),就会调用此方法将链表转化为红黑树,以提高后续操作的性能。
    2. addCount(1L, binCount):如果成功添加了新节点,会调用 addCount 来增加 ConcurrentHashMap 的总大小。这是一个高度优化的计数器,使用分段计数来减少并发争用。

     原子性保证总结

    computeIfAbsent 的原子性是通过以下机制组合实现的:

    1. CAS 操作:用于在无锁或低锁的情况下进行状态转换,例如在空桶中放置 ReservationNode
    2. 分段/分桶锁 (Fine-Grained Locking):当必须加锁时,ConcurrentHashMap 不会锁住整个 Map,而是只锁住需要修改的那个哈希桶的头节点 (synchronized(f))。这使得不同桶的操作可以完全并发进行,大大提高了吞吐量。
    3. volatile 读写tabAt 和 setTabAt 等方法内部使用 VarHandle(在旧版本中是 Unsafe),提供了 volatile 语义的内存读写,确保一个线程对 table 数组的修改对其他线程立即可见。
    4. 协作式扩容:遇到扩容时,线程会主动参与,而不是被动等待,避免了全局暂停。
    5. 将计算置于锁内:最关键的一点是,可能耗时的 mappingFunction 的调用,被严格地保护在 synchronized 块内部。这确保了“计算并放入”这个复合操作的原子性,杜绝了竞态条件。

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值