并发集合-Concurrent系列

ConcurrentHashMap

jdk 1.7

数据结构

ConcurrentHashMap
└── segments: Segment[16]  // 外层数组
    │
    ├── Segment[0]
    │   ├── table: HashEntry[2]  // 内层数组
    │   │   ├── [0] → HashEntry(key="a", value=1) → HashEntry(key="b", value=2) → null
    │   │   └── [1] → null
    │   └── lock: ReentrantLock
    │
    ├── Segment[1]
    │   ├── table: HashEntry[4]  // 扩容后的数组
    │   │   ├── [0] → null
    │   │   ├── [1] → HashEntry(key="c", value=3) → null
    │   │   └── ...
    │   └── lock: ReentrantLock
    └── ...
ConcurrentHashMap {
    final Segment<K,V>[] segments; 
}

static class Segment<K,V> extends ReentrantLock {
    transient volatile HashEntry<K,V>[] table;
}

static final class HashEntry<K,V> {
    final K key;           
    volatile V value;    
    final int hash;      
    final HashEntry<K,V> next;
}

构造方法

public ConcurrentHashMap(int initialCapacity,
                         float loadFactor,
                         int concurrencyLevel) {
    // 1. 参数校验
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();

    // 2. 确保并发级别不超过最大值(1 << 16)
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;

    // 3. 计算Segment数组的容量(向上取整为2的幂)
    int sshift = 0;
    int ssize = 1; // Segment数组的实际大小
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1; // 通过左移保证ssize是2的幂
    }

    // 4. 计算用于定位Segment的掩码和位移量
    this.segmentShift = 32 - sshift; // 用于计算Segment索引的高位掩码
    this.segmentMask = ssize - 1;    // 用于计算Segment索引的低位掩码

    // 5. 计算每个Segment内部HashEntry数组的初始容量
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    int c = initialCapacity / ssize; // 平均分配到每个Segment
    if (c * ssize < initialCapacity)
        ++c; // 向上取整
    int cap = MIN_SEGMENT_TABLE_CAPACITY; // Segment内部HashEntry数组的最小容量(默认为2)
    while (cap < c)
        cap <<= 1; // 保证cap是2的幂

    // 6. 创建第一个Segment实例(其他Segment延迟初始化)
    Segment<K,V> s0 =
        new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                         (HashEntry<K,V>[])new HashEntry[cap]);

    // 7. 初始化Segment数组
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    UNSAFE.putOrderedObject(ss, SBASE, s0); // 使用UNSAFE保证可见性
    this.segments = ss;
}
  • Segment 数组默认长度是16,但初始化时是可以指定的的,concurrencyLevel
  • initialCapacity 是所有 HashEntry 数组的长度和(最小2的幂)
  • initialCapacity / concurrencyLevel 是每个 HashEntry 数组的长度(向上取整也要是2的幂)
  • 因为 Segment 数组不会扩容,所以初始化时只创建第一个 Segment(初始的加载因子,扩容阈值等也就是确定的,所以先创建出来后面直接使用)

特性总结

  • hash 表套娃
    • 第一层 数组是 Segment 类型,Segment 继承了 ReenTrantLock
    • 第二次数组是 HashEntry 类型,类似一个 HashMap
  • Segment 数组默认长度就是并发量,默认长度是16,一旦创建不能扩容和缩容
    • segment 是段的意思,所以也叫分段锁
    • 多个线程操作同一个桶要先获取 ReenTrantLock 锁,锁住的是这个桶(相比于 HashTable 锁住整个 hash 表,支持的并发更高)
  • HashEntry 数组最小长度是 2
    • 可以扩容
    • 拉链法解决 hash 冲突,数据结构是链表,不会涉及红黑树,头插法
  • 两次 hash 运算,第一次定位 Segment 数组,第二次定位 HashEntry 数组

jdk1.8

数据结构上就不是双层 hash 表了,和 HashMap 类似,没啥好说的了直接上源码

put() 添加元素

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException(); // key、value 都不允许为空,HashMap 可以一个空 key,不限 value
    int hash = spread(key.hashCode()); // hash 值
    int binCount = 0; // 和 HashMap 一样,用于后面是否链表转红黑树,表示链表长度
    for (Node<K,V>[] tab = table;;) { // 无限循环
        Node<K,V> f; int n, i, fh;
      
      	// 数组为空就初始化数组,这次循环结束,下次循环时数组就不为空了
        if (tab == null || (n = tab.length) == 0) 
            tab = initTable(); // 初始化数组
      
      	/**
      	 * 走到这里说明数组不为空,那就找到桶,桶里如果没有有元素的处理
      	 * 1. 算出 key 的下标
      	 * 2. 取出下标处的第一个元素(可能只有一个元素,可能是链表,可能是红黑树)
      	 * 3. 如果第一个元素是空,直接 CAS 插入元素
      	 * 4. CAS 成功,流程结束;CAS 失败,本次循环结束,进入下一次循环(下一次循环时,第一个元素就不为空了,就不会进入这个 if)
      	 */
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { 
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) // CAS,没有使用锁
                break;          
        }
      
      	/**
      	 * 桶里有元素的处理(第一个元素不为空就说明桶里已经有元素了),表示 hash 冲突了,但是这里没有处理 hash 冲突
      	 * 第一个元素的 hash 如果是 -1,说明其他线程正在扩容,所以当前线程要等待,但是当前线程不会阻塞,而是去帮忙扩容
      	 * 1. 正常情况下,hash 值是 >=0 的
      	 * 2. MOVED(-1):正在转移元素(也就是处于扩容中,当前桶的元素正在转移到新的数组中)
      	 * 3. TREEBIN(-2):当前桶的元素是 TreeBin(红黑树)
      	 * 4. RESERVED(-3):保留节点
      	 */
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
      
        else { 
          	// 这时才真正处理 hash 冲突
            V oldVal = null;
            synchronized (f) { // 锁住的是第一个元素,表示锁住当前桶(比 1.7 的 Segment 并发性能高)
                if (tabAt(tab, i) == f) { // 再取一次,防止 synchronized 加锁时 f 被改了
                    if (fh >= 0) {  // 根据元素的 hash 值推断出是链表,元素加入链表,尾插法(如果重复就覆盖)
                       // ...
                    }
                    else if (f instanceof TreeBin) { // 是红黑树(元素加入树中,重复也会覆盖)
                        // ...
                    }
                }
            } // 这里 synchronized 的锁就释放了,下面的树化也要保证线程安全,使用的是 CAS 方式
          
          	// 元素可能一开始链表,元素加入链表后,可能需要树化
            if (binCount != 0) {
              	// 链表元素数量 >= 8进行树化(不是立马树化,内部先扩容,扩容后如果数组长度达到64,链表长度还是 >=8 才转红黑树)
                if (binCount >= TREEIFY_THRESHOLD) 
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
  
    addCount(1L, binCount); // size++ 和 扩容检查
    return null;
}

如果元素是红黑树,HashMap 保存的是 TreeNode,ConcurrentHashMap 保存的是 TreeBin 对象,本质没啥区别,都是树

initTable() 初始化数组

要保证线程安全,那就只能一个线程创建数组,使用 CAS 实现的

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; 
    int sc;
    while ((tab = table) == null || tab.length == 0) { // 如果未初始化,进行初始化,但是要保证只能一个线程执行
      
      	// 抢到初始化权的线程会把 sizeCtl 改为 -1,所以这里看看 sizeCtl 是都小于 0,如果是说明当前别的线程已经在初始化了
        if ((sc = sizeCtl) < 0) 
            Thread.yield(); // 当前线程让出 CPU,为什么要让?避免CPU空转把CPU拉满了
      
        // 走到这里说明 sizeCtl 还不是 -1,那就要抢一下初始化权(CAS改为 - 1就表示抢到了;没抢到就进入下一次循环,会让出 CPU)
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                // 双重检查(避免其他线程已初始化完成)
                if ((tab = table) == null || tab.length == 0) {
                    // 计算初始容量(若 sizeCtl > 0 则使用它,否则用默认值 16)
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    // 创建数组
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    // 计算扩容阈值(0.75 * n)
                    sc = n - (n >>> 2);
                }
            } finally {
                // 最终将 sizeCtl 设为阈值,此时 sizeCtl 就是正数了
                sizeCtl = sc;
            }
            break; // 退出自旋
        }
    }
    return tab; // 返回初始化后的 table
}
  • 初始时 sizeCtl 是 0,CAS 成功的线程将其设置为 -1(其他线程看到是 -1 就自旋),数组初始化后 sizeCtl 表示扩容阈值
  • 不做初始化的线程会自旋,并且让出 CPU(Thread.yield())避免 CPU 被打满了

addCount() 维护元素个数和扩容检查

private final void addCount(long x, int check) {
    CounterCell[] as; 
    long b, s; // b: baseCount临时值, s: 总元素数
    
    /*
     * 第一部分:更新元素计数(分治思想)
     * 1. 优先尝试更新 baseCount,失败后使用 CounterCell 数组分散竞争
     * 2. ThreadLocalRandom.getProbe() 为线程分配专属槽位减少冲突
     * 3. 竞争激烈时进入 fullAddCount 处理初始化/扩容/重试
     * ----------------------------------------
     */
    if ((as = counterCells) != null || !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
      
        		CounterCell a; long v; int m;
            boolean uncontended = true;
      
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
              	
                fullAddCount(x, uncontended); // 里面很多东西,有精力再看吧
                return;
            }
            if (check <= 1)
                return;
            s = sumCount(); // size() 方法也会调用这个方法,分治了还要汇总求和
    }

    // 第二部分:检查是否需要扩容
    // ----------------------------------------
    if (check >= 0) {
        Node<K,V>[] tab, nt; int n, sc;
        
        // 自旋检查:元素数 >= 扩容阈值 && 表未达最大容量。需要扩容
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) {
            int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
            
            // 如果sizeCtl < 0(说明已在扩容中)
            if (sc < 0) {
                // 如果扩容是否已完成或无法协助,退出扩容检查
                if (sc == rs + MAX_RESIZERS || sc == rs + 1 || (nt = nextTable) == null || transferIndex <= 0)
                    break;
                
                // CAS 增加扩容线程数,然后协助数据迁移
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            // 当前是首个触发扩容的线程,发起扩容
            else if (U.compareAndSwapInt(this, SIZECTL, sc, rs + 2))
                transfer(tab, null);
            
            // 11. 重新计算元素总数,继续检查
            s = sumCount();
        }
    }
}

transfer() 扩容

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
  
    // 1. 计算每个线程处理的桶区间(最小16个桶)
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE;

    // 2. 初始化新表(nextTab),容量为旧表2倍
    if (nextTab == null) {
        try {
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {
            sizeCtl = Integer.MAX_VALUE; // 扩容失败
            return;
        }
        nextTable = nextTab;
        transferIndex = n; // 从旧表末尾开始迁移
    }

    int nextn = nextTab.length;
    // 3. 创建ForwardingNode标记已迁移的桶
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);

    // 4. 自旋迁移数据
    boolean advance = true;
    boolean finishing = false;
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        // 4.1 分配当前线程处理的桶区间 [bound, i]
        while (advance) {
            if (--i >= bound || finishing)
                advance = false;
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            // CAS更新transferIndex,领取任务区间
            else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex, nextIndex > stride ? nextIndex - stride : 0)) {
                bound = nextIndex - stride;
                i = nextIndex - 1;
                advance = false;
            }
        }

        // 4.2 检查迁移是否完成
        if (i < 0 || i >= n || i + n >= nextn) {
            if (finishing) {
                nextTable = null;
                table = nextTab;
                sizeCtl = (n << 1) - (n >>> 1); // 更新阈值(0.75*新容量)
                return;
            }
            // CAS减少扩容线程数
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return; // 非最后一个线程直接退出
                finishing = advance = true;
                i = n; // 最后线程重新检查所有桶
            }
        }
        // 4.3 处理空桶(标记为ForwardingNode)
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        // 4.4 跳过已迁移的桶
        else if ((fh = f.hash) == MOVED)
            advance = true;
        // 4.5 迁移非空桶(链表或树)
        else {
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    // 链表迁移(保持原顺序)
                    if (fh >= 0) {
                        // 将链表放入新表
                    }
                    // 树节点迁移
                    else if (f instanceof TreeBin) {
                        // 判断是否需要退化为链表
                    }
                }
            }
        }
    }
}

1.7 和 1.8 区别

对比项 JDK 1.7 JDK 1.8
数据结构 双层 hash 表:Segment 数组、HashEntry 数组 + 链表 单层 hash 表:数组+链表/红黑树
锁机制 Segment 分段锁,ReenTrantLock 实现 桶级别,CAS + synchronized 实现
并发度 Segment 数组长度 桶数量
扩容 单线程扩容、只扩容某个 Segment 内的 HashEntry 数组 多线程并发扩容、整体扩容(只有一个数组,类似 HashMap)
Hash 冲突处理 链表 链表或红黑树(当链表长度 ≥ 8 且数组长度 ≥ 64 时转换)
查询性能 遍历链表,O(n) 链表 O(n),红黑树 O(log n)
Null 值 不允许 key 或 value 为 null 同样不允许 key 或 value 为 null
  1. 锁粒度更细:从 Segment 级别 缩小到 Node 级别,减少锁竞争。
  2. 引入红黑树:解决 Hash 冲突导致的链表过长问题,提高查询效率。
  3. CAS 优化:减少锁的使用,提高并发性能。
  4. 扩容优化:支持 多线程协助扩容,提高扩容效率。

ConcurrentSkipListMap

跳表介绍

跳表由多层链表组成:

  • 最底层(Level 0):完整的 有序链表,包含所有元素。
  • 上层(Level 1, 2, ...):是 下层的“索引层”,节点数量逐渐减少,类似于 二分查找的跳跃路径
跳表结构:
L3: HEAD → 1 ---------------------→ 9
L2: HEAD → 1 -------→ 5 -------→ 9
L1: HEAD → 1 → 3 → 5 → 7 → 9
L0: HEAD → 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9

查询路径:
1. L3: 1 → 9(跳过,9>6)
2. L2: 1 → 5 → 9(5<6<9,锁定5)
3. L1: 5 → 7(7>6,下沉)
4. L0: 5 → 6(找到!)
%% 跳表层级结构(清晰展示多层链表) graph LR subgraph Level 3 A3[HEAD] --> B3[1] --------> C3[9] end subgraph Level 2 A2[HEAD] --> B2[1] --> C2[5] --> D2[9] end subgraph Level 1 A1[HEAD] --> B1[1] --> C1[3] --> D1[5] --> E1[7] --> F1[9] end subgraph Level 0 A0[HEAD] --> B0[1] --> C0[2] --> D0[3] --> E0[4] --> F0[5] --> G0[6] --> H0[7] --> I0[8] --> J0[9] end %% 纵向指针连接(跨层级关系) B3 -->|↓| B2 -->|↓| B1 -->|↓| B0 C2 -->|↓| D1 -->|↓| F0 C3 -->|↓| D2 -->|↓| F1 -->|↓| J0

阿萨德

ConcurrentSkipListMap 是 Java 并发包中的一个线程安全的有序映射实现,基于跳表(Skip List)数据结构。

ConcurrentSkipListMap 特点

  1. 线程安全:并发访问不需要外部同步
  2. 有序性:按照键的自然顺序或Comparator指定的顺序排序
  3. 高性能:平均时间复杂度为O(log n)
  4. 无锁读取:读操作不需要加锁

ConcurrentSkipListMap VS TreeMap

特性 ConcurrentSkipListMap TreeMap
线程安全
底层结构 跳表 红黑树
并发性能 低(需外部同步)
范围查询效率 高(链表遍历优势) 中(需要中序遍历)

ConcurrentSkipListMap 基本用法示例

ConcurrentSkipListMap<Integer, String> map = new ConcurrentSkipListMap<>();

// 添加元素
map.put(3, "Three");
map.put(1, "One");
map.put(2, "Two");

// 获取元素
String value = map.get(2);  // "Two"

// 遍历(有序)
for (Map.Entry<Integer, String> entry : map.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 输出:
// 1: One
// 2: Two
// 3: Three

// 并发操作
ExecutorService executor = Executors.newFixedThreadPool(4);
for (int i = 0; i < 10; i++) {
    final int key = i;
    executor.submit(() -> map.put(key, "Value-" + key));
}

ConcurrentLinkedQueue

  • 无界非阻塞队列
  • 基于CAS操作实现
  • 高性能但size()方法开销大

ConcurrentLinkedDeque

  • 无界非阻塞双端队列
  • 可以在两端插入和移除元素
  • 同样基于CAS实现

ConcurrentSkipListSet

  • 基于ConcurrentSkipListMap实现
  • 线程安全的有序Set
  • 类似TreeSet的并发版本
posted @ 2023-05-24 16:57  CyrusHuang  阅读(33)  评论(0)    收藏  举报