志在指尖
用双手敲打未来

为什么ConcurrentHashMap的读操作不需要加锁?

我们知道,ConcurrentHashmap(1.8)这个并发调集结构是线程安全的,当你看到源码的get操作时,会发现get操作全程是没有加任何锁的,这也是这篇博文讨论的问题——为什么它不需要加锁呢?
ConcurrentHashMap的简介
我想有基础的同学知道在jdk1.7中是选用Segment+HashEntry+ReentrantLock的方式进行完成的,而1.8中抛弃了Segment臃肿的规划,取而代之的是选用Node+CAS+Synchronized来确保并发安全进行完成。
JDK1.8的完成下降锁的粒度,JDK1.7版本锁的粒度是根据Segment的,包含多个HashEntry,而JDK1.8锁的粒度便是HashEntry(首节点)
JDK1.8版本的数据结构变得愈加简略,使得操作也愈加明晰流畅,因为现已运用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,因为粒度的下降,完成的复杂度也增加了Java
JDK1.8运用红黑树来优化链表,根据长度很长的链表的遍历是一个很漫长的进程,而红黑树的遍历效率是很快的,替代必定阈值的链表,这样构成一个最佳拍档
img
get操作源码
首先计算hash值,定位到该table索引方位,假如是首节点契合就回来
假如遇到扩容的时分,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就回来
以上都不契合的话,就往下遍历节点,匹配就回来,否则最终就回来null
//会发现源码中没有一处加了锁publicVget(Objectkey){
Node[]tab;Nodee,p;intn,eh;Kek;inth=spread(key.hashCode());//计算hashif((tab=table)!=null&&(n=tab.length)>0&&
(e=tabAt(tab,(n-1)&h))!=null){//读取首节点的Node元素if((eh=e.hash)==h){//假如该节点便是首节点就回来if((ek=e.key)==key||(ek!=null&&key.equals(ek)))returne.val;
}//hash值为负值表示正在扩容,这个时分查的是ForwardingNode的find方法来定位到nextTable来//eh=-1,阐明该节点是一个ForwardingNode,正在搬迁,此刻调用ForwardingNode的find方法去nextTable里找。//eh=-2,阐明该节点是一个TreeBin,此刻调用TreeBin的find方法遍历红黑树,因为红黑树有或许正在旋转变色,所以find里会有读写锁。//eh>=0,阐明该节点下挂的是一个链表,直接遍历该链表即可。elseif(eh<0)return(p=e.find(h,key))!=null?p.val:null;while((e=e.next)!=null){//既不是首节点也不是ForwardingNode,那就往下遍历if(e.hash==h&&
((ek=e.key)==key||(ek!=null&&key.equals(ek))))returne.val;
}
}returnnull;
}
get没有加锁的话,ConcurrentHashMap是如何确保读到的数据不是脏数据的呢?
volatile登场
关于可见性,Java供给了volatile关键字来确保可见性、有序性。但不确保原子性。
一般的同享变量不能确保可见性,因为一般同享变量被修正之后,什么时分被写入主存是不确定的,当其他线程去读取时,此刻内存中或许仍是本来的旧值,因此无法确保可见性。
volatile关键字关于根本类型的修正能够在随后对多个线程的读保持共同,但是关于引证类型如数组,实体bean,仅仅确保引证的可见性,但并不确保引证内容的可见性。。
禁止进行指令重排序。
布景:为了提高处理速度,处理器不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。
假如对声明了volatile的变量进行写操作,JVM就会向处理器发送一条指令,将这个变量地点缓存行的数据写回到系统内存。但是,就算写回到内存,假如其他处理器缓存的值仍是旧的,再履行计算操作就会有问题。
在多处理器下,为了确保各个处理器的缓存是共同的,就会完成缓存共同性协议,当某个CPU在写数据时,假如发现操作的变量是同享变量,则会通知其他CPU告知该变量的缓存行是无效的,因此其他CPU在读取该变量时,发现其无效会从头从主存中加载数据。
img
总结下来:
第一:运用volatile关键字会强制将修正的值立即写入主存;
第二:运用volatile关键字的话,当线程2进行修正时,会导致线程1的作业内存中缓存变量的缓存行无效(反映到硬件层的话,便是CPU的L1或许L2缓存中对应的缓存行无效);
第三:因为线程1的作业内存中缓存变量的缓存行无效,所以线程1再次读取变量的值时会去主存读取。
是加在数组上的volatile吗?
/**
*Thearrayofbins.Lazilyinitializeduponfirstinsertion.
*Sizeisalwaysapoweroftwo.Accesseddirectlybyiterators.
*/
transientvolatileNode[]table;
我们知道volatile能够润饰数组的,只是意思和它表面上看起来的姿态不同。举个栗子,volatileintarray[10]是指array的地址是volatile的而不是数组元素的值是volatile的.
用volatile润饰的Node
get操作能够无锁是因为Node的元素val和指针next是用volatile润饰的,在多线程环境下线程A修正结点的val或许新增节点的时分是对线程B可见的。
staticclassNode<K,V>implementsMap.Entry<K,V>{finalinthash;finalKkey;//能够看到这些都用了volatile润饰volatileVval;volatileNodenext;
Node(inthash,Kkey,Vval,Nodenext){this.hash=hash;this.key=key;this.val=val;this.next=next;
}publicfinalKgetKey(){returnkey;}publicfinalVgetValue(){returnval;}publicfinalinthashCode(){returnkey.hashCode()^val.hashCode();}publicfinalStringtoString(){returnkey+”=”+val;}publicfinalVsetValue(Vvalue){thrownewUnsupportedOperationException();
}publicfinalbooleanequals(Objecto){
Objectk,v,u;Map.Entrye;return((oinstanceofMap.Entry)&&
(k=(e=(Map.Entry)o).getKey())!=null&&
(v=e.getValue())!=null&&
(k==key||k.equals(key))&&
(v==(u=val)||v.equals(u)));
}/**
*Virtualizedsupportformap.get();overriddeninsubclasses.
*/Nodefind(inth,Objectk){
Nodee=this;if(k!=null){do{
Kek;if(e.hash==h&&
((ek=e.key)==k||(ek!=null&&k.equals(ek))))returne;
}while((e=e.next)!=null);
}returnnull;
}
}
既然volatile润饰数组对get操作没有作用那加在数组上的volatile的目的是什么呢?
其实便是为了使得Node数组在扩容的时分对其他线程具有可见性而加的volatile
总结
在1.8中ConcurrentHashMap的get操作全程不需要加锁,这也是它比其他并发调集比如hashtable、用Collections.synchronizedMap()包装的hashmap;安全效率高的原因之一。
get操作全程不需要加锁是因为Node的成员val是用volatile润饰的和数组用volatile润饰没有关系。
数组用volatile润饰主要是确保在数组扩容的时分确保可见性。

未经允许不得转载:IT技术网站 » 为什么ConcurrentHashMap的读操作不需要加锁?
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!

 

志在指尖 用双手敲打未来

登录/注册IT技术大全

热门IT技术

C#基础入门   SQL server数据库   系统SEO学习教程   WordPress小技巧   WordPress插件   脚本与源码下载