java基础-ConcurrentHashMap

ConcurrentHashMap 是 Java 并发包 (java.util.concurrent) 中提供的一个线程安全的高性能哈希表 实现。它是 HashMap 的并发版本,专为多线程环境设计,无需外部同步即可安全地在多个线程中同时读写。

类层次结构

完整的继承体系

java 复制代码
// ConcurrentHashMap 的声明
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {
    // 实现细节...
}

// 类图关系
java.lang.Object
    └── java.util.AbstractMap<K,V>
        └── java.util.concurrent.ConcurrentHashMap<K,V>

实现的接口

  1. Map<K,V> - 基础映射接口

  2. ConcurrentMap<K,V> - 并发映射接口(扩展 Map

  3. Serializable - 可序列化接口

核心接口详解

1. Map 接口(基础契约)

ConcurrentHashMap 首先是一个 Map,提供了所有映射的基本操作:

java 复制代码
// Map 核心方法(ConcurrentHashMap 全部实现)
interface Map<K,V> {
    V put(K key, V value);
    V get(Object key);
    V remove(Object key);
    boolean containsKey(Object key);
    int size();
    boolean isEmpty();
    void clear();
    Set<K> keySet();
    Collection<V> values();
    Set<Map.Entry<K,V>> entrySet();
    // ... 其他方法
}

2. ConcurrentMap 接口(并发增强)

ConcurrentMapMap 基础上增加了原子性复合操作

java 复制代码
// ConcurrentMap 扩展的方法(针对并发设计)
interface ConcurrentMap<K,V> extends Map<K,V> {
    // 1. 仅当键不存在时插入
    V putIfAbsent(K key, V value);
    
    // 2. 仅当键存在且值匹配时删除
    boolean remove(Object key, Object value);
    
    // 3. 仅当键存在且旧值匹配时替换
    boolean replace(K key, V oldValue, V newValue);
    
    // 4. 替换指定键的值
    V replace(K key, V value);
}

实现细节:继承 AbstractMap

AbstractMap 的作用

AbstractMap 提供了 Map 接口的骨架实现,简化了具体实现类的工作:

java 复制代码
// AbstractMap 提供的基础实现
public abstract class AbstractMap<K,V> implements Map<K,V> {
    // 1. 提供 entrySet() 的默认实现框架
    public abstract Set<Entry<K,V>> entrySet();
    
    // 2. 基于 entrySet() 实现其他方法
    public V get(Object key) {
        // 遍历 entrySet 查找键
    }
    
    public V put(K key, V value) {
        throw new UnsupportedOperationException();
    }
    
    public int size() {
        return entrySet().size();
    }
    
    public boolean isEmpty() {
        return size() == 0;
    }
    // ... 其他通用实现
}

ConcurrentHashMap 如何扩展 AbstractMap

ConcurrentHashMap 重写了几乎所有方法,因为并发场景需要特殊处理:

java 复制代码
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> 
    implements ConcurrentMap<K,V> {
    
    // 1. 不依赖父类的 entrySet() 实现
    // 2. 所有核心方法都是独立重新实现
    // 3. 添加并发特有的字段和方法
    
    // 重写 size() 方法(特殊并发处理)
    public int size() {
        // 不是简单的 entrySet().size()
        // 而是分段/分桶统计的近似值
        long n = sumCount();
        return ((n < 0L) ? 0 :
                (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
                (int)n);
    }
    
    // 实现 ConcurrentMap 的原子方法
    public V putIfAbsent(K key, V value) {
        return putVal(key, value, true);
    }
}

设计模式:模板方法模式

这种继承结构体现了模板方法模式

  • AbstractMap:定义算法骨架

  • ConcurrentHashMap:提供具体实现,但为并发优化而大幅重写

为什么不直接实现 Map?

原因 1:代码复用

尽管 ConcurrentHashMap 重写了很多方法,但仍能复用 AbstractMap 的一些辅助方法:

java 复制代码
// AbstractMap 中可复用的通用实现
public String toString() {
    // 使用 entrySet() 遍历(ConcurrentHashMap 已实现)
}

public boolean equals(Object o) {
    // 比较逻辑可复用
}

public int hashCode() {
    // 基于 entrySet() 计算哈希值
}

原因 2:契约保证

继承 AbstractMap 表明它遵循 Map 的标准契约。

原因 3:工具类兼容

一些通用工具方法基于 AbstractMap 实现:

java 复制代码
// Collections 中的工具方法
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
    return new SynchronizedMap<>(m);
}
// 这些方法对 AbstractMap 子类有特殊优化

ConcurrentHashMap 的特殊设计

字段结构(Java 8+)

java 复制代码
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> {
    // 核心存储:volatile 保证可见性
    transient volatile Node<K,V>[] table;
    
    // 扩容时的新表
    private transient volatile Node<K,V>[] nextTable;
    
    // 基础计数器(用于 size())
    private transient volatile long baseCount;
    
    // 控制变量(多用途:大小、扩容等)
    private transient volatile int sizeCtl;
    
    // 视图(延迟初始化)
    private transient KeySetView<K,V> keySet;
    private transient ValuesView<K,V> values;
    private transient EntrySetView<K,V> entrySet;
}

视图实现

ConcurrentHashMap 有自己的视图实现,而非使用 AbstractMap 的默认视图:

java 复制代码
// 自定义的线程安全视图
static final class KeySetView<K,V> extends CollectionView<K,V,K>
    implements Set<K>, java.io.Serializable {
    // 弱一致性的迭代器
    public final Iterator<K> iterator() {
        return new KeyIterator();
    }
}

// 通过这些方法返回视图
public KeySetView<K,V> keySet() {
    KeySetView<K,V> ks;
    if ((ks = keySet) != null) return ks;
    return keySet = new KeySetView<>(this, null);
}

与其他并发容器的比较

继承关系 特点
ConcurrentHashMap AbstractMapConcurrentHashMap 分段/桶锁,高并发
ConcurrentSkipListMap AbstractMapConcurrentSkipListMap 跳表,有序,无锁读
Hashtable DictionaryHashtable 全表锁,遗留类
Collections.synchronizedMap 包装器模式 全表锁,包装现有 Map

总结关系

java 复制代码
Map (接口) <-- 实现契约
    ↑
ConcurrentMap (接口) <-- 添加原子操作
    ↑
ConcurrentHashMap (具体类)
    ↑
AbstractMap (抽象类) <-- 提供部分默认实现(但大部分被重写)

关键点

  1. ConcurrentHashMap 一个 Map,提供所有映射功能

  2. 实现了 ConcurrentMap,增加了原子复合操作

  3. 继承AbstractMap,但为并发性能几乎重写了所有方法

  4. 这种设计既保持了接口兼容性,又允许针对并发场景深度优化

这就是为什么你可以在需要 Map 的地方使用 ConcurrentHashMap,同时获得线程安全的额外好处。

主要特点

1. 线程安全且高效

  • HashtableCollections.synchronizedMap() 的全局锁不同,ConcurrentHashMap 使用分段锁 (Java 7)或 CAS + synchronized(Java 8+)实现细粒度并发控制

  • 读操作通常无需加锁(除了扩容等特殊情况)

2. 弱一致性迭代器

  • 迭代器创建后不反映所有更新操作(与 HashMap 的快速失败迭代器不同)

  • 不会抛出 ConcurrentModificationException

3. 不允许 null 键或值

  • 避免二义性:map.get(key) 返回 null 时,无法区分是键不存在还是值为 null

核心实现原理

Java 7 实现(分段锁)

java 复制代码
// Java 7 的 ConcurrentHashMap 结构
public class ConcurrentHashMap<K, V> {
    // 核心:Segment 数组
    final Segment<K,V>[] segments;
    
    static final class Segment<K,V> extends ReentrantLock {
        // 每个 Segment 有自己的哈希表
        HashEntry<K,V>[] table;
        int count;  // Segment 内元素数量
    }
    
    static final class HashEntry<K,V> {
        final K key;
        volatile V value;
        final int hash;
        final HashEntry<K,V> next;
    }
}
  • 将整个表分为多个段(Segment)

  • 每个段独立加锁,不同段可并发操作

分段锁原理

java 复制代码
// 简化的写入流程
public V put(K key, V value) {
    int hash = hash(key);
    // 1. 根据 hash 确定 Segment
    int segmentIndex = (hash >>> segmentShift) & segmentMask;
    Segment<K,V> segment = segments[segmentIndex];
    
    // 2. 对该 Segment 加锁
    segment.lock();
    try {
        // 3. 在 Segment 内部的哈希表中操作
        int bucketIndex = (hash >>> segment.bucketShift) & segment.bucketMask;
        HashEntry<K,V> first = segment.table[bucketIndex];
        // ... 插入逻辑
    } finally {
        segment.unlock();
    }
}

分段锁的特点

  • 默认 16 个段(可通过构造函数设置)

  • 不同段可以并发写入

  • 同一段内操作需要竞争锁

  • 读操作通常无锁(volatile 保证可见性)

Java 8+ 实现(CAS + synchronized)

主要变革

Java 8 抛弃了分段锁,采用更细粒度的并发控制:

java 复制代码
// Java 8 的 ConcurrentHashMap 核心结构
public class ConcurrentHashMap<K,V> {
    // 单个哈希表(不再是分段)
    transient volatile Node<K,V>[] table;
    
    // 桶节点
    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
    }
    
    // 红黑树节点(当链表过长时转换)
    static final class TreeNode<K,V> extends Node<K,V> {
        TreeNode<K,V> parent;
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;  // 删除时需要取消链接
        boolean red;
    }
}

Java 8 的并发控制技术

1. CAS(Compare And Swap)无锁算法

java 复制代码
// 使用 CAS 的典型场景
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                    Node<K,V> c, Node<K,V> v) {
    return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

// 应用:插入新节点(桶为空时)
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
    break;  // 插入成功

2. synchronized 锁单个桶

java 复制代码
// 当桶不为空时,使用 synchronized 锁住链表头/树根
Node<K,V> f = tabAt(tab, i);
synchronized (f) {  // 锁住桶的第一个节点
    if (tabAt(tab, i) == f) {  // 双重检查
        // 链表操作或树操作
        if (fh >= 0) {  // 链表
            // 遍历链表...
        } else if (f instanceof TreeBin) {  // 红黑树
            // 树操作...
        }
    }
}

3. 扩容时的并发控制

java 复制代码
// 扩容期间的特殊处理
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    // 多个线程可以协助扩容
    // 每个线程处理一个桶区间
    while (advance) {
        // 分配桶区间给当前线程
    }
    
    // 转移节点
    synchronized (f) {  // 锁住原桶
        // 将节点拆分到新表的两个桶中
    }
}

ava 8 的具体并发场景

场景 1:读操作(完全无锁)

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) {
        
        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;
}

场景 2:写操作(CAS + synchronized)

java 复制代码
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // ... 省略部分代码
    
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();  // 初始化表(CAS)
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // CASE 1: 桶为空,使用 CAS 插入新节点
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;  // 插入成功
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);  // 协助扩容
        else {
            V oldVal = null;
            // CASE 2: 桶不为空,锁住桶头节点
            synchronized (f) {
                if (tabAt(tab, i) == f) {  // 双重检查
                    if (fh >= 0) {  // 链表
                        // ... 链表插入/更新
                    }
                    else if (f instanceof TreeBin) {  // 红黑树
                        // ... 树插入/更新
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);  // 转换为红黑树
                break;
            }
        }
    }
    // ... 更新计数等
}

锁的精确粒度:桶级别

java 复制代码
// Java 8 ConcurrentHashMap 的数组结构
Node<K,V>[] table = new Node[16];  // 哈希表数组

// 每个桶可能是以下三种状态之一:
// 1. null - 空桶
// 2. Node  - 链表头节点
// 3. TreeBin - 红黑树的包装节点(持有树根)

三种情况的具体锁机制

情况 1:桶为空(无锁 CAS 操作)

java 复制代码
// 当桶为空时,使用 CAS 无锁插入
if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    // 使用 CAS 原子操作插入新节点
    if (casTabAt(tab, i, null, 
                 new Node<K,V>(hash, key, value, null)))
        break; // 插入成功
}

无锁操作:CAS 保证原子性

情况 2:桶为链表(锁链表头节点)

java 复制代码
// 当桶是链表时,锁住链表头节点
synchronized (f) {  // f 是链表第一个节点
    if (tabAt(tab, i) == f) {  // 双重检查
        // 遍历链表进行操作
        for (Node<K,V> e = f;; ++binCount) {
            // ... 插入、更新或删除节点
        }
    }
}

🔒 锁对象 :链表第一个 Node 对象

情况 3:桶为红黑树(锁 TreeBin 节点)

java 复制代码
// 当桶是红黑树时,锁住 TreeBin 节点
else if (f instanceof TreeBin) {
    TreeBin<K,V> t = (TreeBin<K,V>)f;
    synchronized (t) {  // 锁住 TreeBin 对象
        // 在红黑树中插入、查找或删除
        TreeNode<K,V> p = t.putTreeVal(hash, key, value);
    }
}

🔒 锁对象TreeBin 对象(不是树根 TreeNode

关键设计细节

1. 为什么锁第一个节点而不是整个数组?

java 复制代码
// 错误示例:全局锁(性能差)
synchronized (table) {  // ❌ 锁住整个数组
    // 所有操作串行化
}

// 正确示例:桶级别锁
synchronized (table[i]) {  // ✅ 只锁一个桶
    // 其他桶的操作可以并发进行
}

2. 双重检查锁定模式

java 复制代码
Node<K,V> f = tabAt(tab, i);  // 第一次读取
synchronized (f) {  // 锁住 f
    if (tabAt(tab, i) == f) {  // 第二次检查(确保 f 仍然是桶头)
        // 执行操作
    }
}

3. 锁对象的具体身份

java 复制代码
// 数组中的元素可能是:
// 1. Node(链表)
table[3] = new Node<>(hash, key, value, next);

// 2. TreeBin(红黑树的包装器)
table[5] = new TreeBin<>();

// 3. ForwardingNode(扩容时的特殊标记)
table[2] = new ForwardingNode<>(nextTable);

对比表格:Java 7 vs Java 8

特性 Java 7(分段锁) Java 8(CAS + synchronized)
锁粒度 段级别(默认16段) 桶级别(更细粒度)
锁机制 ReentrantLock synchronized + CAS
数据结构 数组+链表 数组+链表+红黑树
读并发 无锁(volatile读) 无锁(volatile读)
写并发 不同段可并发写 不同桶可并发写
扩容 段内独立扩容 协助扩容(多线程协作)
内存开销 较高(Segment对象) 较低
性能 段竞争时下降 竞争更少,吞吐量更高

Java 8 的关键优化

1. 红黑树优化链表

  • 当链表长度 > 8 且表大小 ≥ 64 时,转换为红黑树

  • 查找复杂度从 O(n) 降为 O(log n)

2. 扩容并发协助

java 复制代码
// 多个线程可以协助扩容
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int stride = (NCPU > 1) ? (n >>> 3) / NCPU : n;
    // 每个线程处理一个桶区间,并行迁移
}

3. 计数机制优化

java 复制代码
// 使用 CounterCell 数组分散计数争用
@sun.misc.Contended static final class CounterCell {
    volatile long value;
    CounterCell(long x) { value = x; }
}

// 避免单个计数器的争用
final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

实际性能对比

java 复制代码
// 基准测试示例
public class Benchmark {
    public static void main(String[] args) {
        // Java 7: 在高并发写时,如果键都hash到同一个段,性能会下降
        // Java 8: 更细粒度的锁,即使hash冲突,只要不在同一个桶,仍可并发
    }
}

结论

  • Java 7 :分段锁是粗粒度的并发控制,段数固定可能成为瓶颈

  • Java 8 :CAS + synchronized 是更细粒度 的控制,配合红黑树和协助扩容,在高并发场景下性能显著提升,特别是在哈希冲突较多时

这就是为什么 Java 8 的 ConcurrentHashMap 能支持更高的并发度,同时保持更好的性能表现。

相关推荐
早日退休!!!8 小时前
进程与线程的上下文加载_保存及内存映射
开发语言
jllllyuz8 小时前
MATLAB实现蜻蜓优化算法
开发语言·算法·matlab
冰暮流星8 小时前
javascript逻辑运算符
开发语言·javascript·ecmascript
flysh059 小时前
如何利用 C# 内置的 Action 和 Func 委托
开发语言·c#
一嘴一个橘子9 小时前
spring-aop 的 基础使用 - 4 - 环绕通知 @Around
java
小毅&Nora9 小时前
【Java线程安全实战】⑨ CompletableFuture的高级用法:从基础到高阶,结合虚拟线程
java·线程安全·虚拟线程
码农小韩9 小时前
基于Linux的C++学习——动态数组容器vector
linux·c语言·开发语言·数据结构·c++·单片机·学习
冰冰菜的扣jio9 小时前
Redis缓存中三大问题——穿透、击穿、雪崩
java·redis·缓存
木风小助理9 小时前
`mapfile`命令详解:Bash中高效的文本至数组转换工具
开发语言·chrome·bash