ConcurrentHashMap设计原理

本文深入探讨了ConcurrentHashMap的设计原理,包括其高效性的体现,如并发增加元素、并发扩容、红黑树与链表的使用,以及分段锁等特性。通过源码分析,展示了其在JDK1.7和1.8的不同实现,如1.8引入的红黑树以及取消分段锁。此外,文章还介绍了ConcurrentHashMap在Spring和Tomcat等场景的应用,以及其在并发场景下的高效操作,如computeIfAbsent、computeIfPresent和merge方法的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


前言

很赞的ConcurrentHashMap流程图 https://www.processon.com/view/5f7439d05653bb06efe8b219
compareAndSet
方法主要调用unsafe.compareAndSwapInt(Object object, long offset, int expect, int update)
4个参数:
其中第一个参数为需要改变的对象。
第二个为偏移量(即之前求出来的valueOffset的值)。
第三个参数为期待的值。
第四个为更新后的值。
整个方法的作用即为若调用该方法时,value的值与expect这个值相等,那么则将value修改为update这个值,并返回一个true,如果调用该方法时,value的值与expect这个值不相等,那么不做任何操作,并范围一个false。

一、ConcurrentHashMap简介【CHM】

ConcurrentHashMap是conccurrent并发包中的一个类,由于它可以高效地支持并发操作,以及被广泛使用,经典的开源框架Spring的底层数据结构就是使用ConcurrentHashMap实现的。与同是线程安全的老大哥HashTable相比,它已经更胜一筹,因此它的锁更加细化,而不是像HashTable一样为几乎每个方法都添加了synchronized锁,这样的锁无疑会影响到性能。
注释:新版ConcurrentHashMap摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。它沿用了与它同时期的HashMap版本的思想,底层依然由“数组”+链表+红黑树的方式思想,但是为了做到并发,又增加了很多辅助的类,例如TreeBin,Traverser等对象内部类。

应用场景:
1、Spring中Bean的管理
2、Tomcat中Session管理
那么问题来了:ConcurrentHashMap的哪些设计体现了它的高效性?
高效性总结:
1、通过数组的方式来实现并发增加元素的个数
2、并发扩容,可以通过多个线程来并行实现数据迁移。
3、采用高低位链的方式解决多次hash计算的问题,提升了效率
4、sizeCtl的设计,3中状态。不同场景含义不同
负数代表正在进行初始化或扩容操作
-1代表正在初始化,占位符,表示已经有线程在初始化table
-N 表示有N-1个线程正在进行扩容操作
5、resizeStamp的设计,高低位的设计来实现唯一性以及多个线程的协助扩容记录
6、红黑树、链表
7、分段锁

二、ConcurrentHashMap的使用

computeIfAbsent
computeIfPresent
compute (它是computeIfAbsent 和 computeIfPresent 两者结合)
merge

public static void main(String[] args){
	ConcurrentMap<String,Integer> chm = new ConcurrentHashMap<String,Integer>();
	//如果key不存在则调用函数式接口k->1 ,把结果作为value放入map
	chm.computeIfAbsent("chm",k->1);
	//如果key存在则修改,调用函数式接口的返回值放进去,不存在返回null
	chm.computeIfPresent("chm",(k,v)->v+1);
	// Integer::sum 代表调用Integer的静态方法sum
	Stream.of(1,2,3,4,6,2,3,6,8,1).forEach(v->{
		chm.merge(v,5,Integer:sum)
	});
	System.out.println(chm);
}
//结果: {1=10,2=10,3=10,4=5,6=10,8=5}

merge是计算合并,即chm中存在多个key的合并,5是concurrentHashMap中的value,调用函数式接口来执行合并,即调用Integer的静态方法sum 求和

三、ConcurrentHashMap源码分析

ConcurrentHashMap的数据结构:数组+链表/红黑树
在这里插入图片描述
JDK1.7 和 JDK1.8 关于CHM的实现方式不同有哪些?
1、jdk1.8引入红黑树,查询的时间复杂度由 O(n) --> O(log)
2、jdk1.8取消了分段锁,jdk1.7采用分段锁,锁粒度较大。jdk1.8是在数组的某一个node加锁
transient java关键字
其实这个关键字的作用很好理解,就是简单的一句话:将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会被序列化
链表转红黑树条件:1、数组长度大于64 2、链表长度大于8。如果大于则调用treeifyBin 看看是否需要链表转红黑树
程序入口:
public V put(K key, V value)
final V putVal(K key, V value, boolean onlyIfAbsent)
putVal第一阶段
整个putVal 可以分为几段去理解:
1、判断HashMap的Tab数组是否初始化,未初始化时初始化。
2、根据hashcode计算tab数组下标中的数据是否为null,如果是则直接写入
3、判断节点是否正在扩容,如果是则帮助扩容
4、对tab数组的某个节点加锁,判断节点是用链表存储还是红黑树存储并分别处理
5、判断binCount即链表长度是否大于8

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
     int hash = spread(key.hashCode());  //通过key值计算hash值
     int binCount = 0;   //用来记录链表的长度
     for (Node<K,V>[] tab = table;;) {  //自旋,线程出现竞争时不断自旋
         Node<K,V> f; int n, i, fh;
         if (tab == null || (n = tab.length) == 0)
             tab = initTable(); //初始化table
//通过 hash 值对应的数组下标得到第一个节点; 
//tabAt:以 volatile 读的方式来读取 table 数组中的元素,保证每次拿到的数据都是最新的
//------------第2阶段-----------------------
         else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
         //如果该下标返回的节点为空,则直接通过 cas 将新的值封装成 node 插入即可;如果 cas 失败,说明存在竞争,则进入下一次循环。由于volatile语义,假如存在多线程改动value,value值可见只有一个能cas成功
             if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
                 break;//没有并发、没有锁的时候执行到此跳出自旋// no lock when adding to empty bin
         }
         //--------如果放入位置已有的元素的hash为-1 则代表Map正在扩容,加入帮助扩容
         //------------第3阶段-----------------------
         else if ((fh = f.hash) == MOVED)
          //如果下表内有node,且该node的hash值为moved(即-1),说明当前concurrenthashmap正在扩容,那么本次操作将会帮助扩容
             tab = helpTransfer(tab, f);
         else { //------------第4阶段-----------------------
             V oldVal = null;
             // f 指的是根据需要放入map的key计算出hash值&上数组下标得出来 
             synchronized (f) { //对hashMap数组,初始化16个元素的某一个元素加锁,锁的粒度更细。【分布式思想】
                 if (tabAt(tab, i) == f) {
                     //fh为该node的hash值,如果hash值为正数,说明该下标是按链表存储的
                     if (fh >= 0) {
                         binCount = 1;
                         for (Node<K,V> e = f;; ++binCount) {
                             K ek;
                             //当放入节点的hash以及key相等则直接覆盖
                             if (e.hash == hash &&
                                 ((ek = e.key) == key ||(ek != null && key.equals(ek)))) {
                                 oldVal = e.val;
                                // putIfAbsent()保存数据的时候,如果该链表中保存的有相同key的值,那么就不会对我们当前的value进行保存,如果用put()存储数据的时候,不管是该链表中是否有当前需要存储的key都会保存
                                 if (!onlyIfAbsent) //可以通过onlyIfAbsent决定是否覆盖
                                     e.val = value;
                                 break;
                             }
                             Node<K,V> pred = e;
                     //e.next==null说明当前节点没有下一个节点,则直接插入尾部,构建链表。
                             if ((e = e.next) == null) {
                                 pred.next = new Node<K,V>(hash, key,value, null);
                                 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;
                         }
                     }
                 }
             }
            //binCount统计链表深度,如果大于TREEIFY_THRESHOLD:8 则调用treeifBin
             if (binCount != 0) {
                 if (binCount >= TREEIFY_THRESHOLD)
                     treeifyBin(tab, i);
                 if (oldVal != null)
                     return oldVal;
                 break;
             }
         }
     }
      //总node数+1,该方法中若满足条件会进行扩容
     addCount(1L, binCount);
     return null;
 }

spread分析

    static final int spread(int h) {
        //参看下面的图:把key的hashcode值右移16位之后。^ 异或   h ^右移后的hashcode 
        return (h ^ (h >>> 16)) & HASH_BITS;
    }

int hash = spread(key.hashCode()); //通过key值计算hash值
在这里插入图片描述

treeifBin分析
1、判断tab长度是否小于 MIN_TREEIFY_CAPACITY:64 如果是则扩容。否则转红黑树
2、synchronized (b) 对tab数组的单个节点加锁

private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {
            //如果tab长度小于64则调用tryPresize扩容数组
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                tryPresize(n << 1); //扩容
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
                synchronized (b) {
                //大于64,红黑树转换
                    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));
                    }
                }
            }
        }
    }

**tryPresize 分析 **
扩容:1、多线程并发扩容
扩容的本质:创建新的数组,把老的数据迁移到新数组
sizeCtl定义及注释
sizeCtl :默认为0,用来控制table的初始化和扩容操作
-1 代表table正在初始化
-N 表示有 N对应的二进制的低16位数值为M,此时有M-1个线程进行扩容。
其余情况:
1、如果table未初始化,表示table需要初始化的大小。
2、如果table初始化完成,表示table的容量,默认是table大小的0.75倍

能修改sizeCtl的方法有五个:
initTable()
addCount()
tryPresize()
transfer()
helpTransfer()

private final void tryPresize(int size) {
       //用来计算扩容的大小
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
            tableSizeFor(size + (size >>> 1) + 1); //tableSizeFor是用来整形的,如果创建ConcurrentHashMap传入长度15,则通过此方法改变为最接近2的n次方,整形为16
        int sc;
        while ((sc = sizeCtl) >= 0) { //说明要做数组的初始化,因为tryPresize在putAll里面有调用
            Node<K,V>[] tab = table; int n;
            if (tab == null || (n = tab.length) == 0) {
                n = (sc > c) ? sc : c; //比较初始化容量与扩容容量,谁大取谁。因为putAll可能数量量比较大,此时初始化容量就不一定是16了
                if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                    try {
                        if (table == tab) {
                            @SuppressWarnings("unchecked")
                            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                            table = nt;
                            sc = n - (n >>> 2);
                        }
                    } finally {
                        sizeCtl = sc;
                    }
                }
            }
            //如果已经是最大容量不能扩容,直接break
            else if (c <= sc || n >= MAXIMUM_CAPACITY)
                break;
            else if (tab == table) {
                int rs = resizeStamp(n);//生成扩容戳,保证当前扩容的唯一性。
                if (sc < 0) { //第一次扩容不走这段逻辑
                    Node<K,V>[] nt;
                    //if判断成功表示扩容结束
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    //扩容没有结束,sc+1,每增加一个扩容线程 sc+1
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                //第一次扩容走这段逻辑
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
            }
        }
    }

<< 带符号左移,<<< 无符号左移
stamp扩容戳:1000 0000 0000 0000 0000 0000 0001 1100
扩容戳带符号左移16位 1000 0000 0001 1100 0000 0000 0000 0000
高16位表示当前扩容的标记,保证唯一性
低16位表示当前扩容的线程的数量
rs << RESIZE_STAMP_SHIFT) + 2 扩容戳左移16位后+2
1000 0000 0001 1100 0000 0000 0000 0010 表示当前有1个线程扩容。

扩容结束后,通过rs << RESIZE_STAMP_SHIFT - 1 ,当stamp的低16位减为0,表示扩容结束
必须有地方记录,在扩容的范围内有多少线程参与扩容。所有线程都完成数据迁移后才代表扩容完成。

resizeStamp
Integer.numberOfLeadingZeros 返回无符号整数最高位非0位前面0的个数

    static final int resizeStamp(int n) {
        return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
    }

举例:n为8 ,8的32位无符号二进制表示为 0000 0000 0000 0000 0000 0000 0000 1000
无符号整数最高位非0位前面0的个数,即1前面有28个0,即28的二进制表示为:
0000 0000 0000 0000 0000 0000 0001 1100 或上 1左移15位,即
1000 0000 0000 0000 0000 0000 0000 0000 结果为:1000 0000 0000 0000 0000 0000 0001 1100 【这个就是扩容戳】
当然根据扩容戳的反向计算也能得出 tab容量

initTable分析
初始化table

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
     while ((tab = table) == null || tab.length == 0) {
         if ((sc = sizeCtl) < 0) //被其他线程抢占了初始化的操作,则直接让出自己的 CPU时间片
             Thread.yield(); // lost initialization race; just spin
         else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { 
         //if条件中sc=sizeCtl,所以此处cas成功
             try {
                 if ((tab = table) == null || tab.length == 0) {
                 //CAS把默认DEFAULT_CAPACITY:16
                     int n = (sc > 0) ? sc : DEFAULT_CAPACITY; 
                     @SuppressWarnings("unchecked")
                     Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                     table = tab = nt;
         //sc存的是扩容阈值:计算下次扩容的大小,实际就是当前容量的 0.75倍,这里使用了右移来计算,16-4=12
                     sc = n - (n >>> 2);
                 }
             } finally {
              //设置 sizeCtl 为 sc, 如果默认是 16 的话,那么这个时候sc=16*0.75=12
                 sizeCtl = sc;
             }
             break;
         }
     }
     return tab;
 }

tabAt分析

static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}

该方法获取对象中offset偏移地址对应的对象field的值。实际上这段代码的含义等价于tab[i], 但是为什么不直接使用 tab[i]来计算呢?
U.getObjectVolatile【unsafe封装的原子操作】,一旦看到 volatile 关键字,就表示可见性。因为对 volatile 写操作 happens-before 于 volatile 读操作,因此其他线程对 table 的修改均对 get 读取可见; transient volatile Node<K,V>[] table;虽然 table 数组本身是增加了 volatile 属性,但是“volatile 的数组只针对数组的引用具有volatile 的语义,而不是它的元素”。 所以如果有其他线程对这个数组的元素进行写操作,那么当前线程来读的时候不一定能读到最新的值。出于性能考虑,直接通过 Unsafe 类来对 table 进行操作。

putVal第二阶段
在putVal方法执行完成以后,会通过addCount来增加ConcurrentHashMap中的元素个数,
并且还会可能触发扩容操作。这里会有两个非常经典的设计

  1. 高并发下的扩容
  2. 如何保证 addCount 的数据安全性以及性能
    //将当前 ConcurrentHashMap 的元素数量加 1,有可能触发 transfer 操作(扩容)
    addCount(1L, binCount);

addCount分析
在 putVal 最后调用 addCount 的时候,传递了两个参数,分别是 1 和 binCount(链表长度),addCount 方法里面做了什么操作?
x 表示这次需要在表中增加的元素个数,check 参数表示是否需要进行扩容检查,大于等于 0 都需要进行检查
addCount是putVal后size+1 ,但是addCount 存在并发场景,并发场景下如何保证安全性?
1、CAS 自旋? 没有使用CAS的原因是高并发性大批量自旋会导致过多线程自旋,浪费CPU资源
2、加锁会影响性能。

private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
       /**判断 counterCells 是否为空,baseCount默认是0
		1. 如果counterCells为空,就通过 cas 操作尝试修改 baseCount 变量,对这个变量进行原子累加操作(做这个操作的意义是:如果在没有竞争的情况下,仍然采用 baseCount 来记录元素个数)
		2. 如果 cas 失败说明存在竞争,这个时候不能再采用 baseCount 来累加,而是通过CounterCell 来记录
		3. Java a==b||(a=b+c)==c 或运算,如果a==b满足条件,则不执行(a=b+c)==c的判断,也就是a没有被b+c赋值 **/
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;//是否冲突标识,默认为没有冲突
           /** 这里有几个判断
			1. 计数表为空则直接调用 fullAddCount
			2. 从计数表中随机取出一个数组的位置为空,直接调用 fullAddCount
			3. 通过 CAS 修改 CounterCell 随机位置的值,如果修改失败说明出现并发情况(这里又用到了一种巧妙的法),调用 fullAndCount。 
			4. Random在线程并发的时候会有性能问题以及可能会产生相同的随机数,ThreadLocalRandom.getProbe 并发安全,并且性能要比 Random 高,通过getProbe() & m 与运算的形式,【m代表数组长度】数组下标一定会落在数组长度区间内**/
            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) //链表长度小于等于 1,不需要考虑扩容
                return;
            s = sumCount(); //统计 ConcurrentHashMap 元素个数
        }
        if (check >= 0) { //check:传递过来的是链表长度binCount
            Node<K,V>[] tab, nt; int n, sc;
            //当sumCount求和得到个数大于等于扩容阈值sizeCtl=0.75*总长度,并且table为node数组,不为空,并且小于最大值时扩容
            //初始化时table为长度为16的node数组,存放hashmap数据
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n); //详见resizeStamp分析
                if (sc < 0) { // 有线程在扩容,
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0)
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                //第一次,没有线程在扩容时,直接修改sc,
                else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }

counterCells.

**CounterCells 解释** ConcurrentHashMap 是采用 CounterCell 数组来记录元素个数的,像一般的集合记录集合大小,直接定义一个 size 的成员变量即可,当出现改变的时候只要更新这个变量就行。为什么ConcurrentHashMap 要用这种形式来处理呢? 问题还是在并发上,ConcurrentHashMap 是并发集合,如果用一个成员变量来统计元素个数的话,为了保证并发情况下共享变量的的安全性,势必会需要通过加锁或者自旋来实现,如果竞争比较激烈的情况下,size 的设置上会出现比较大的冲突反而影响了性能,所以在ConcurrentHashMap 采用了分片的方法来记录大小: ConcurrentHashMap采用:ConuntCell 数组,类似分片形式存储。类似于把多线程分别分配到ConuntCell 的不同元素上记录,最后统计总size。如下图所示: **通过CounterCell分流、扩容,即便是分流到某一个数组元素下存在并发CAS自旋,也不影响其他元素增加【其他线程不用自旋】,通过这种方式来提高性能。** ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190813152630211.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2h1aTExNzA5MDc1MDE=,size_16,color_FFFFFF,t_70)
// 标识当前 cell 数组是否在初始化或扩容中的CAS 标志位
private transient volatile int cellsBusy;
// counterCells 数组,总数值的分值分别存在每个 cell 中
private transient volatile CounterCell[] counterCells;
@sun.misc.Contended static final class CounterCell {
	volatile long value;
	CounterCell(long x) { value = x; }
}
//上面代码表明 CounterCell 数组的每个元素,都存储一个元素个数,而实际我们调用size 方法就是通过这个循环累加来得到的
//又是一个设计精华,大家可以借鉴; 有了这个前提,再会过去看 addCount 这个方法,就容易理解一些了
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;
}

fullAddCount 源码分析

private final void fullAddCount(long x, boolean wasUncontended) {
        int h;
        //获取当前线程的 probe 的值,如果值为 0,则初始化当前线程的 probe 的值,probe 就是随机数
        if ((h = ThreadLocalRandom.getProbe()) == 0) {
            ThreadLocalRandom.localInit();      // force initialization
            h = ThreadLocalRandom.getProbe();
            wasUncontended = true; // 由于重新生成了 probe,未冲突标志位设置为 true
        }
        boolean collide = false;                // True if last slot nonempty
        for (;;) {
            CounterCell[] as; CounterCell a; int n; long v;
            //说明 counterCells 已经被初始化过了,我们先跳过这个代码,先看初始化部分:初始化 cellsbusy=0
            if ((as = counterCells) != null && (n = as.length) > 0) {
            // 通过该值与当前线程 probe 求与,获得cells 的下标元素,和 hash 表获取索引是一样的
                if ((a = as[(n - 1) & h]) == null) {
                    if (cellsBusy == 0) { //cellsBusy=0 表示 counterCells 不在初始化或者扩容状态下
                        CounterCell r = new CounterCell(x);  //构造一个 CounterCell 的值,传入元素个数
                        //通过 cas 设置 cellsBusy 标识,防止其他线程来对 cellsBusy 并发修改
                        if (cellsBusy == 0 && U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                            boolean created = false;
                            try {               // Recheck under lock
                                CounterCell[] rs; int m, j;
                                //将初始化的 r 对象的元素个数放在对应下标的位置
                                if ((rs = counterCells) != null &&(m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {//恢复标志位
                                cellsBusy = 0;
                            }
                            if (created)//创建成功,退出循环
                                break;
                            continue;   //说明指定 cells 下标位置的数据不为空,则进行下一次循环
                        }
                    }
                    collide = false;
                }
                //说明在 addCount 方法中 cas 失败了,并且获取 probe 的值不为空
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;  //设置为未冲突标识,进入下一次自旋
                //由于指定下标位置的 cell 值不为空,则直接通过 cas 进行原子累加,如果成功,则直接退出
                else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                    break;
 				//如果已经有其他线程建立了新的 counterCells 或者 CounterCells 大于 CPU 核心数(很巧妙,线程的并发数不会超过 cpu 核心数)
                else if (counterCells != as || n >= NCPU)
                    collide = false;        //设置当前线程的循环失败不进行扩容
                else if (!collide)  //恢复 collide 状态,标识下次循环会进行扩容
                    collide = true; 
                //进入这个步骤,说明 CounterCell 数组容量不够,线程竞争较大,所以先设置一个标识表示为正在扩容
                else if (cellsBusy == 0 &&
                         U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                    try {
                        if (counterCells == as) {// Expand table unless stale
                            //扩容一倍 2 变成 4,这个扩容比较简单
                            CounterCell[] rs = new CounterCell[n << 1];
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            counterCells = rs;
                        }
                    } finally {
                        cellsBusy = 0; //恢复标识
                    }
                    collide = false;
                    continue;   //继续下一次自旋
                }
                h = ThreadLocalRandom.advanceProbe(h); //更新随机数的值
            }
            //cellsBusy=0 表示没有在做初始化,通过 cas 更新cellsbusy 的值标改为“1”,表示当前线程正在做CounterCell初始化操作
            else if (cellsBusy == 0 && counterCells == as && U.compareAndSwapInt(this, CELLSBUSY, 0, 1)){
                boolean init = false;
                try {                           // Initialize table
                    if (counterCells == as) {
                        CounterCell[] rs = new CounterCell[2]; //初始化容量为 2 的CounterCell
                     // h是随机数&(数组长度-1)得到数组下标,x:是1,初始化时CounterCell包装1放入CounterCell数组
                        rs[h & 1] = new CounterCell(x); //将 x 也就是元素的个数放在指定的数组下标位置
                        counterCells = rs; //赋值给 counterCells
                        init = true; ;//设置初始化完成标识
                    }
                } finally {
                    cellsBusy = 0; //恢复标识
                }
                if (init)
                    break;
            }
            //竞争激烈,其它线程占据 cell 数组,直接累加在 base 变量中
            else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
                break;                          // Fall back on using base
        }
    }

这段代码挺长的,初始化逻辑是从 else if (cellsBusy == 0 && counterCells == as && U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) 开始的,先从此向下看。
ConterCell初始化过后,fullAddCount再次调用时

resizeStamp分析

static final int resizeStamp(int n) {
    return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}

n :第一次是16
RESIZE_STAMP_BITS : 16 静态int常量
通过把n=16带入方法resizeStamp 可以计算出rs
第一次扩容时执行:rs等于 0000 0000 0000 0000 1000 0000 0001 1011
U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)
这句代码把rs左移16位后加2
左移16位后:1000 0000 0001 1011 0000 0000 0000 0000
+2后 :1000 0000 0001 1011 0000 0000 0000 0010
结果:扩容戳
高16位代表扩容标记
低16位代表扩容线程数,+2表示有一个线程
1、每次扩容的扩容戳都是唯一的
2、可以支持并发扩容【多线程扩容】
扩容戳能够记录在某个周期内有n个线程参与扩容。【在容量为16的周期内存在n-1个线程并发扩容】
为什么 +2 ?
sizeCtl -1:代表初始化
2:代表一个线程扩容
sizeCtl 高位为1,为负数,所以一定小于0

transfer 扩容阶段
扩容:
1、扩大数组长度
2、数据迁移

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    /**  NCPU:代表CPU核心数, n=16 右移3
    ** 将 length / 8 然后除以 CPU核心数。如果得到的结果小于 16,那么就使用 16。
    这里的目的是让每个 CPU 处理的桶一样多,避免出现转移任务不均匀的现象,如果桶较少的话,默认一个 CPU(一个线程)处理 16 个桶
	**/
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    //第一次扩容,nextTab 传递的为null
    if (nextTab == null) {            // initiating
        try {
            @SuppressWarnings("unchecked")
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; //n左移1位,扩大2倍,即32
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        nextTable = nextTab;
        transferIndex = n;
    }
    int nextn = nextTab.length;
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); //占位符,表示已经迁移过了
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab
    //定义每个线程扩容的边界,多线程扩容
    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;
            }
            // nextBound=16   bound =16  i=32-1=31
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            if (finishing) {
                nextTable = null;
                table = nextTab;
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                finishing = advance = true;
                i = n; // recheck before commit
            }
        }
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {
            synchronized (f) {  //对数组该节点位置加锁,开始处理数组该位置的迁移工作
                if (tabAt(tab, i) == f) {  //再做一次校验
                //ln 表示低位, hn 表示高位;接下来这段代码的作用是把链表拆分成两部分,0 在低位,1 在高位
                    Node<K,V> ln, hn; 
                    if (fh >= 0) {
                        int runBit = fh & n;
                        Node<K,V> lastRun = f;
                        //遍历当前 bucket 的链表,目的是尽量重用 Node 链表尾部的一部分
                        for (Node<K,V> p = f.next; p != null; p = p.next) {
                            int b = p.hash & n;
                            if (b != runBit) {
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        if (runBit == 0) {//如果最后更新的 runBit 是 0,设置低位节点
                            ln = lastRun;
                            hn = null;
                        }
                        else {//否则,设置高位节点
                            hn = lastRun;
                            ln = null;
                        }
                        //构造高位以及低位的链表
                        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);
                        }
                        setTabAt(nextTab, i, ln); //将低位的链表放在 i 位置也就是不动
                        setTabAt(nextTab, i + n, hn); //将高位链表放在 i+n 位置
                        setTabAt(tab, i, fwd);// 把旧table的hash桶中放置转发节点,表明此 hash 桶已经被处理
                        advance = true;
                    }
                    //红黑树的扩容部分暂时忽略
                    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;
                        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;
                            }
                        }
                        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;
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                }
            }
        }
    }
}

高低位原理分析

ConcurrentHashMap 在做链表迁移时,会用高低位来实现,这里有两个问题要分析一下

  1. 如何实现高低位链表的区分
    假如我们有这样一个队列
    在这里插入图片描述
    第 14 个槽位插入新节点之后,链表元素个数已经达到了 8,且数组长度为 16,优先通过扩容
    来缓解链表过长的问题,扩容这块的图解稍后再分析,先分析高低位扩容的原理
    假如当前线程正在处理槽位为 14 的节点,它是一个链表结构,在代码中,首先定义两个变量节点 ln 和 hn,实际就是 lowNode 和 HighNode,分别保存 hash 值的第 x 位为 0 和不等于0 的节点。通过 fn&n 可以把这个链表中的元素分为两类,A 类是 hash 值的第 X 位为 0,B 类是 hash 值的第 x 位为不等于 0(至于为什么要这么区分,稍后分析),并且通过 lastRun 记录最后要处理的节点。最终要达到的目的是,A 类的链表保持位置不动,B 类的链表为 14+16(扩容增加的长度)=30 我们把 14 槽位的链表单独伶出来,我们用蓝色表示 fn&n=0 的节点,假如链表的分类是这样
    在这里插入图片描述
 for (Node<K,V> p = f.next; p != null; p = p.next) {
   int b = p.hash & n;
   if (b != runBit) {
       runBit = b;
       lastRun = p;
   }
}

通过上面这段代码遍历,会记录 runBit 以及 lastRun,按照上面这个结构,那么 runBit 应该是蓝色节点,lastRun 应该是第 6 个节点。接着,再通过这段代码进行遍历,生成 ln 链以及 hn 链

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

在这里插入图片描述
接着,通过 CAS 操作,把 hn 链放在 i+n 也就是 14+16 的位置,ln 链保持原来的位置不动。并且设置当前节点为 fwd,表示已经被当前线程迁移完了
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);

### ConcurrentHashMap 的内部原理与工作机制 ConcurrentHashMap 是 Java 中用于支持高效并发访问的哈希表实现。它的设计目标是在多线程环境下提供高性能的读写操作,同时避免数据不一致的问题。以下是其内部实现的关键点: #### 1. 分段锁机制(Segment Lock) 在早期版本的 Java(如 Java 7)中,ConcurrentHashMap 使用了一种分段锁(Segment Lock)的机制来实现并发控制。整个哈希表被划分为多个段(Segment),每个段是一个独立的小型哈希表[^3]。当进行写操作时,ConcurrentHashMap 只需要锁定相关的段,而不需要锁定整个表,从而提高了并发性能。 #### 2. CAS 操作与无锁设计 从 Java 8 开始,ConcurrentHashMap 的实现发生了重大变化。它摒弃了分段锁机制,转而采用基于 CAS(Compare-And-Swap)操作的无锁设计[^2]。这种设计通过原子操作来确保线程安全,减少了锁的使用频率和范围,进一步提升了并发性能。 #### 3. 哈希计算与桶分布 ConcurrentHashMap 在插入或查找元素时,首先会根据键的 `hashCode` 计算出一个初始哈希值。然后,通过 `spread()` 方法对这个初始哈希值进行二次扰动,以减少哈希冲突的可能性[^4]。最终的哈希值决定了该键值对应该存储在哪个桶中。 #### 4. 动态扩容 ConcurrentHashMap 支持动态扩容,以适应不断增长的数据量。在扩容过程中,ConcurrentHashMap 会将原有的桶重新分配到新的桶数组中。为了保证扩容过程中的线程安全性,Java 8 的实现引入了 `ForwardingNode` 节点[^2]。这些节点在扩容期间起到临时转发的作用,确保读操作能够正确地找到目标元素。 #### 5. 写操作的线程安全 ConcurrentHashMap 的写操作(如 `put` 和 `remove`)通过以下方式确保线程安全: - **CAS 操作**:对于简单的更新操作,ConcurrentHashMap 使用 CAS 操作来避免显式锁定。 - **局部锁定**:在复杂情况下(如链表转换为红黑树或扩容),ConcurrentHashMap 会对特定的桶进行短暂的锁定,而不是锁定整个表。 #### 6. 遍历操作的线程安全 ConcurrentHashMap 的遍历操作(如 `forEach` 或 `keySet().iterator()`)是弱一致性的,这意味着在遍历过程中允许其他线程修改哈希表的内容。这种设计在大多数情况下可以满足需求,同时避免了因锁定导致的性能下降[^2]。 ### 示例代码 以下是一个简单的示例,展示如何使用 ConcurrentHashMap 并理解其基本操作: ```java import java.util.concurrent.ConcurrentHashMap; public class ConcurrentHashMapExample { public static void main(String[] args) { ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); // 插入键值对 map.put("one", 1); map.put("two", 2); map.put("three", 3); // 遍历 ConcurrentHashMap for (String key : map.keySet()) { System.out.println("Key: " + key + ", Value: " + map.get(key)); } // 并发安全的更新操作 map.computeIfAbsent("four", k -> 4); map.computeIfPresent("two", (k, v) -> v * 2); // 输出结果 System.out.println(map); } } ``` ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值