数据结构论坛

首页 » 分类 » 分类 » 转载ConcurrentHashMa
TUhjnbcbe - 2024/10/29 13:28:00

一、简介

上篇文章详细介绍了HashMap的源码及原理,本文趁热打铁继续分析ConcurrentHashMap的原理。

首先在看本文之前,希望对HashMap有一个详细的了解。不然看直接看ConcurrentHashMap的源码还是有些费劲的。

相信对HashMap,HashTable有一定了解,应该知道HashMap是不具备线程安全性的,在resize时会丢数据(JDK8),而HashTable虽然保证了线程安全性,但是其是通过给每个方法加Synchronized关键字达到的同步目的。但是都知道Synchronized在竞争激烈的多线程并发环境中,在性能上的表现是非常不如人意的。那在高并发环境中HashMap如何保证线程安全而又不浪费太多性能呢?答案就是JavaJ.U.C并发包中的ConcurrentHashMap。

依然开局一张图。JDK8中的ConcurrentHashMap数据结构。

呃呵,和HashMap的结构是一样的,没错在数据结构层面,ConcurrentHashMap和HashMap是完全一样的。有了这个基础继续往下看。

二、历史版本

ConcurrentHashMap的历史版本大致分界线在JDK8。也就是可以分为JDK8和JDK8以前版本。

数据结构的区别

在JDK8之前HashMap没有引入红黑树,同样的ConcurrentHashMap也没有引入红黑树。而且ConcurrentHashMap采用的是分段数组的底层数据结构。

在JDK7中的数据结构。

从上图我们不难看出其在数据结构方面的差别。

锁的区别

JDK7中为了提高并发性能采用了这种分段的设计。所以在JDK7中ConcurrentHashMap采用的是分段锁,也就是在每个Segment上加ReentrantLock实现的线程安全线。关于ReetrantLock后面有时间会介绍,大致来说ReetrantLoack是比Synchronized更细粒度的一种锁。使用得当的话其性能要比Synchronized表现要好,但是如果实现不得当容易造成死锁。

这种基于Segment和ReetrantLock的设计相对HashTable来说大大提高了并发性能。也就是说多个线程可以并发的操作多个Segment,而HashTable是通过给每个方法加Synchronized即将多线程串行而实现的。所以在一定程度上提高了并发性能。但是这种性能的提升表现相对JDK8来说显得不值一提。

如果说JDK7ConcurrentHashMap相对HashTable来说是串行到多个线程并发的改进。而JDK8则是通过比Segment更细粒度的并发控制大大提高了其并发表现。

JDK8中ConcurrentHashMap采用的是CAS+Synchronized锁并且锁粒度是每一个桶。简单来说JDK7中锁的粒度是Segment,JDK8锁粒度细化到了桶级别。可想而知锁粒度是大大提到了。辅之以代码的优化,JDK8中的ConcurrentHashMap在性能上的表现非常优秀。

简单总结一下,从HashTable到JDK7ConcurrentHashMap再到JDK8ConcurrentHashMap。是从同步到并发再到高并发的进步。

三、基础知识

3.1、常量

//正在扩容,对应fwd类型的节点的hashstaticfinalintMOVED=-1;//hashforforwardingnodes//当前数组transientvolatileNode[]table;//扩容时用到的,扩容后的数组。privatetransientvolatileNode[]nextTable;//1,大于零,表示size*0.75。//2,等于-1,表示正在初始化。//3,-(n+1),表示正在执行扩容的线程其只表示基数,而不是真正的数量,需要计算得出的哦privatetransientvolatileintsizeCtl;

3.2、Unsafe类方法

1

SuppressWarnings(unchecked)//transientvolatileNode[]table;tab变量确实是volatile2staticfinalNodetabAt(Node[]tab,inti){//获取table中索引i处的元素。3return(Node)U.getObjectVolatile(tab,((long)i//如果tab是volatile变量,则该方法保证其可见性。4}56staticfinalbooleancasTabAt(Node[]tab,inti,//通过CAS设置table索引为i处的元素。7Nodec,Nodev){8returnU.
  4.1、如果正在扩容,协助其它线程去扩容
  4.2、如果是链表,插入链表
  4.3、如果是红黑树,插入红黑树
  4.4、如果链表长度超过8,树化
  4.5,如果key已经存在,覆盖旧值5,需要扩容,则扩容

相比HashMap过程多了一个协助扩容。

以上源码需要注意的是

1for(Node[]tab=table;;){//迭代桶数组,自旋23}

这是一个自旋的过程,如果CAS修改失败会进入下一轮自旋。很久以前看这段源码的时候,我总是在想,CAS失败了不就丢数据了吗?所以这个自旋,也称之为自旋锁会保证数据一定能插入成功。

说说上面锁竞争的情况,以上过程我们不难发现对table的修改都是通过CAS操作实现的。比如下面这行代码,如果已经有线程正在操作i位置的元素,则意味着本轮自旋将会失败,继续自旋,当其他线程修改完成,本线程再次运行到tabAt以为是Volatile操作,其他线程的修改对本线程立即可见(详见Volatile关键字内存语义的内容)。本线程通过tabAt发现该处已经存在元素,即发生碰撞,继续往下运行。

1elseif((f=tabAt(tab,i=(n-1)hash))==null){2if(casTabAt(tab,i,null,newNode(hash,key,value,null)))//通过cas插入新值3break;//nolockwhenaddingtoemptybin4}

线程的调度需要操作系统从用户态转为内核态,这是非常重量级的操作。CAS+自旋组成的自旋锁保证了线程不会进入阻塞态。

然后继续往下看

synchronized(f){//再次检查当前节点是否有变化,有变化进入下一轮自旋//为什么再次检查?因为不能保证,当前线程运行到这里,有没有其他线程对该节点进行修改if(tabAt(tab,i)==f){

先看这行代码synchronized(f)这个f是一个桶的头元素。也就是说在JDK8中synchronized锁仅仅只锁链表头或者红黑树的头(其实就是锁一个桶,因为要访问链表或者红黑树总要从头开始访问吧)

再看if(tabAt(tab,i)==f){}其实就是双重检测(参考单例的双重检测),为什么要再检查一遍呢?因为不能保证当前线程运行到这里,有没有其他线程已经对该节点进行了修改。

initTable()

1privatefinalNode[]initTable(){2Node[]tab;intsc;3while((tab=table)==null

tab.length==0){4//赋值sc。并当sizeCtl==-1即当前有线程正在执行初始化5if((sc=sizeCtl))6//yield()暂停当前正在执行的线程,执行其他线程7//(这是一个通知,但是这是不一定会让当前线程停止,要取决于线程调度器)8//就是我想让出资源,但是这只是一厢情愿的事情,线程调度器会考虑你的方法,但是不一定采纳。9Thread.yield();10//修改sizeCtl的值为-1。SIZECTL为sizeCtl的内存地址。11elseif(U.
  原创不易,转载请注明原文

1
查看完整版本: 转载ConcurrentHashMa