前言
在多线程并发编程中,HashMap 并不是线程安全的,而 Hashtable 虽然线程安全,但由于其使用 synchronized 修饰所有方法,导致并发性能极差。为了解决这一问题,ConcurrentHashMap 应运而生。
ConcurrentHashMap 是一个线程安全且性能效率比较高的 Map 集合。它利用 synchronized、volatile、CAS 等机制实现线程安全,同时最大程度地减少了锁竞争对性能的影响。
本文总结ConcurrentHashMap 的数据结构演变,并对其核心的 putVal() 和 get() 方法源码进行解读。
一、数据结构
在 JDK1.8 中,ConcurrentHashMap 摒弃了 Segment 分段锁的设计,数据结构演变为 数组 + 链表 + 红黑树。
- 原理:它使用 synchronized 和 CAS 来进行并发控制。允许多个线程并发访问 ConcurrentHashMap。
- 锁粒度:只有在产生哈希冲突后,synchronized 才会锁定产生哈希冲突的链表或红黑二叉树的头节点,极大地控制了锁的范围。
- 优势:只要不产生哈希冲突,就不会产生锁竞争,极大提高了并发效率。最大并发数量等于数组的大小(Node 数组的长度)。
二、putVal() 方法解读
putVal() 是 ConcurrentHashMap 添加元素的核心方法(在 JDK1.8 中,put() 方法直接调用 putVal())。
1.核心思路
- 计算 hash 值,判断数组是否初始化,未初始化则进行初始化。
- 如果当前数组索引位置为空,利用 CAS 操作直接插入节点(无需加锁)。
- 如果当前位置不为空(发生哈希冲突),使用 synchronized 锁定当前节点。
- 判断当前节点是链表还是红黑树,进行相应的遍历插入或更新操作。
- 如果链表长度达到阈值,转换为红黑树。
- 最后更新元素个数,并判断是否需要扩容。
1.关键源码解析
代码如下(示例):
finalVputVal(Kkey,Vvalue,booleanonlyIfAbsent){if(key==null||value==null)thrownewNullPointerException();inthash=spread(key.hashCode());// 1. 计算hash值intbinCount=0;for(Node<K,V>[]tab=table;;){Node<K,V>f;intn,i,fh;// 2. 判断数组是否为空,为空则初始化if(tab==null||(n=tab.length)==0)tab=initTable();// 3. 如果当前索引位置为空,使用CAS无锁插入elseif((f=tabAt(tab,i=(n-1)&hash))==null){if(casTabAt(tab,i,null,newNode<K,V>(hash,key,value,null)))break;// 插入成功,跳出循环}// 4. 如果当前节点正在扩容(hash值为MOVED),帮助扩容elseif((fh=f.hash)==MOVED)tab=helpTransfer(tab,f);// 5. 发生哈希冲突,锁定当前头节点else{VoldVal=null;synchronized(f){// 只锁当前链表或红黑树的头节点if(tabAt(tab,i)==f){if(fh>=0){// 当前节点是链表节点binCount=1;for(Node<K,V>e=f;;++binCount){Kek;// 如果找到相同key,覆盖valueif(e.hash==hash&&((ek=e.key)==key||(ek!=null&&key.equals(ek)))){oldVal=e.val;if(!onlyIfAbsent)e.val=value;break;}Node<K,V>pred=e;// 遍历到链表尾部,插入新节点if((e=e.next)==null){pred.next=newNode<K,V>(hash,key,value,null);break;}}}elseif(finstanceofTreeBin){// 当前节点是红黑树节点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;}}}}if(binCount!=0){// 6. 如果链表长度达到阈值(8),转换为红黑树if(binCount>=TREEIFY_THRESHOLD)treeifyBin(tab,i);if(oldVal!=null)returnoldVal;break;}}}// 7. 元素个数加1,并检查是否需要扩容addCount(1L,binCount);returnnull;}三、get() 方法解读
get() 方法的设计相对简单,因为它的读取操作是不加锁的,这得益于 Node 节点中 val 和 next 指针都被 volatile 修饰,保证了内存可见性。
1.核心思路
- 计算 hash 值。
- 如果数组为空或当前索引位置为空,直接返回 null。
- 如果首节点匹配,直接返回首节点的值。
- 如果首节点 hash 值小于 0(说明是红黑树节点或正在扩容),调用对应的 find() 方法查找。
- 如果首节点是普通链表节点,遍历链表查找匹配的 key。
2.关键源码解析
代码如下(示例):
publicVget(Objectkey){Node<K,V>[]tab;Node<K,V>e,p;intn,eh;Kek;inth=spread(key.hashCode());// 1. 计算hash值// 2. 判断数组是否为空,以及当前索引位置是否有节点if((tab=table)!=null&&(n=tab.length)>0&&(e=tabAt(tab,(n-1)&h))!=null){// 3. 如果首节点的hash值和key都匹配,直接返回if((eh=e.hash)==h){if((ek=e.key)==key||(ek!=null&&key.equals(ek)))returne.val;}// 4. 如果首节点hash值小于0,说明是红黑树节点(TreeBin)或ForwardingNodeelseif(eh<0)return(p=e.find(h,key))!=null?p.val:null;// 5. 遍历链表查找while((e=e.next)!=null){if(e.hash==h&&((ek=e.key)==key||(ek!=null&&key.equals(ek))))returne.val;}}returnnull;}该处使用的url网络请求的数据。