通过源码理解JDK8的ConcurrentHashMap的get方法、put方法以及扩容过程

本文详细分析了JDK8中ConcurrentHashMap的get方法、put方法以及扩容过程。在get操作中,通过无锁方式寻找节点;put操作涉及初始化、CAS创建新节点、扩容协助等;扩容时,创建新表或迁移已有节点。整个过程展示了ConcurrentHashMap的并发安全策略。

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

这天,笔者独自回忆着最近的面试过程,想起面试官问道:你一般用哪些Java集合呀?
我:‍‍🙋‍HashMap。
面试官:那你说说ConcurrentHashMap的扩容过程吧。
我:
在这里插入图片描述
最后面试官语重心长地说:年轻人要多看源码,搞懂底层逻辑,才能越走越远啊!

行吧,那今天就来好好看一看ConcurrentHashMap。
在这里插入图片描述


众所周知,JDK8中ConcurrentHashMap取消了Segment分段锁,采⽤CASsynchronized来保证并发安全。数据结构跟 HashMap1.8的结构类似,采用数组+链表/红黑二叉树,即整个ConcurrentHashMap为一个Node[],Node[]的每一个元素均为首节点,首节点后面为链表或是红黑树。Java 8在链表⻓度超过⼀定阈值(8)时将链表(寻 址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N)))。

下面通过源码对JDK8的ConcurrentHashMap的get方法、put方法以及扩容过程进行分析。

get流程

public V get(Object key) {
   
   
 	Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
 	// spread 方法能确保返回结果是正数
 	int h = spread(key.hashCode());
 	if ((tab = table) != null && (n = tab.length) > 0 &&
 		(e = tabAt(tab, (n - 1) & h)) != null) {
   
   
 		// 如果头结点已经是要查找的 key
 		if ((eh = e.hash) == h) {
   
   
 			if ((ek = e.key) == key || (ek != null && key.equals(ek)))
 				return e.val;
 		}
 		// hash 为负数表示该 bin 在扩容中或是 treebin, 这时调用 find 方法来查找
 		else if (eh < 0)
 			return (p = e.find(h, key)) != null ? p.val : null;
 		// 正常遍历链表, 用 equals 比较
 		while ((e = e.next) != null) {
   
   
 			if (e.hash == h &&
 				((ek = e.key) == key || (ek != null && key.equals(ek))))
 				return e.val;
 		}
 	}
 	return null;
}

总体上看为无锁操作,流程为:

  1. 通过spread方法处理过的hash值找到Node[] tab的一个Node链表的头结点
  2. 如果该链表并不在扩容状态下且头结点就是要找的key,直接返回节点的val
  3. 如果该链表处于扩容状态中或是该链表之前已经被改造为红黑树,则调用find方法来进行查找
  4. 如果该链表既不在扩容状态下,同时头结点也不是要找的key,则遍历链表,最终找到目标节点并返回结点的val

put流程

public V put(K key, V value) {
   
   
    return putVal(key, value, false);
}

final V putVal(K key, V value, boolean onlyIfAbsent) {
   
   
    if (key == null || value == null) throw new NullPointerException();
    // 其中 spread 方法会综合高位低位, 具有更好的 hash 性
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
   
   
        // f 是链表头节点
		// fh 是链表头结点的 hash
 		// i 是链表在 table 中的下标
        Node<K,V> f; int n, i, fh; K fk; V fv;
        // 要创建 table
        if (tab == null || (n = tab.length) == 0)
            // 初始化 table 使用了 cas, 无需 synchronized 创建成功, 进入下一轮循环
            tab = initTable();
        // 要创建链表头节点
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
   
   
            // 添加链表头使用了 cas, 无需 synchronized
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                break;                   // no lock when adding to empty bin
        }
        // 帮忙扩容
        else if ((fh = f.hash) == MOVED)
            // 帮忙之后, 进入下一轮循环
            tab = helpTransfer(tab, f);
        else if (onlyIfAbsent // check first node without acquiring lock
                 && fh == hash
                 && ((fk = f.key) == key || (fk != null 
JDK7中的ConcurrentHashMapJava中的线程安全的哈希表实现,它支持高并发的读写操作。下面是对其源码的简要介绍: 1. ConcurrentHashMap的内部结构: ConcurrentHashMap的内部结构由一个Segment数组和一个HashEntry数组组成。Segment是一种可重入锁,用于对HashEntry数组中的元素进行加锁操作。每个Segment维护了一个HashEntry数组的子集,不同的Segment之间可以并发地进行读写操作。 2. HashEntry的结构: HashEntry是ConcurrentHashMap中存储键值对的节点,它包含了键、值和一个指向下一个节点的引用。当多个键值对映射到同一个桶,它们会形成一个链表。 3. ConcurrentHashMapput操作: 当调用put方法ConcurrentHashMap中插入键值对,首先会根据键的哈希值找到对应的Segment,然后在该Segment中进行插入操作。如果插入的键已经存在,则会更新对应的值;如果插入的键不存在,则会创建一个新的节点并插入到链表的头部。 4. ConcurrentHashMapget操作: 当调用get方法ConcurrentHashMap中获取值,首先会根据键的哈希值找到对应的Segment,然后在该Segment中进行查找操作。如果找到了对应的节点,则返回节点的值;如果没有找到,则返回null。 5. ConcurrentHashMap扩容: 当ConcurrentHashMap中的元素数量达到一定阈值,会触发扩容操作。扩容过程会创建一个新的Segment数组和HashEntry数组,并将原来的元素重新分配到新的数组中。在扩容过程中,读操作可以继续进行,而写操作会被阻塞。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值