它是HashMap的多线程版本,是线程安全的。可以在并发环境下直接使用。为什么放着好好的HashTable不用又要从新实现一个ConcurrentHashMap呢?就是因为效率问题(虽然你安全,但是太慢了,没有爽感。)这个就不一样,即安全,效率还高,这酸爽你能顶的住?

ConcurrentHashMap是将数据分成一段一段存储,然后给每个数据段配一把锁,当一个线程访问占用启用一个数据段的同时,其它数据段也能被其他线程访问。(这种思想就类比公共厕所里的一排蹲坑就行。)

注意:ConcurrentHashMap读不加锁,下文会有解释。


ConcurrentHashMap之内部数据结构

JDK1.7:

ConcurrentHashMap数据结构107

ConcurrentHashMap是由segment数组结构和HashEntry组成,segment实现了ReentrantLock,所以扮演了一个可重入锁的角色。HashEntry用于存储键值对数据。

一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。

注意:初始化时容器里面默认的锁的个数,也就是Segment数组长度为16;具体解释可以看《Java并发编程艺术》P279

图片出处见参考


JDK1.8:

ConcurrentHashMap数据结构108

ConcurrentHashMap取消了分段锁,采用的CAS+Synchronized来保证并发安全。

数据结构和HashMap1.8的类似,数组+链表/红黑树。当链表长度大于8时,会将链表转为红黑树,来保证查询效率。 Synchronized只锁定当前链表的头节点,或红黑树的首节点。

图片出处见参考


ConcurrentHashMap的get()方法

Segment的get操作非常高效,先经过一次再散列,然后使用这个散列值通过散 列运算定位到Segment,再通过散列算法定位到元素。整个过程时不加锁的,除非读到的值是空才会加锁重读。这就是比HashTable效率高的其中一个点。

为什么不(带)加(TT)锁,还能保证安全呢?

就是因为它的get方法里将要使用的共享变量都定义成了volatile类型了,(这操作真骚,直接从根儿上做了绝育)例如用来统计segment大小的count字段,和用于存储HashEntry的值value

现在我们解释一下为啥定义成了volatile类型就可以保证安全了呢?

首先,volatile类型的变量可以保证线程之间的修改可见性,能够被多线程同时读取,并且还保证你读的值不过期,这就很骚。

但是有利有弊。缺点就是只能被单线程写,只有一种情况下可以被多线程写,呢就是要写入的值不依赖于原值。

为什么可以保证读到的值不是过期值呢?

因为happens-before原则嘛,任意对volatile类型变量的写,happens-before对这个变量的读。换成人话就是对volatile类型的变量的写操作优先于读操作。


ConcurrentHashMap的put()方法

put方法需要对共享变量进行写操作,所以需要对操作的共享变量加锁,(真刀真枪的时候,还是加锁放心)。put方法是首先定位到具体的Segment,然后在Segment里进行数据的写入操作。整个的写入操作分为两步

第一步:首先判断是否需要扩容

插入元素之前,先判断Segment种的HashEntry数组是否达到阈值(threshlod),如果超过阈值则对数组进行扩容。(看到这里的时候想一想HashMap是先插入在判断,还是先判断再插入)在扩容时,首先会创建一个容量是原来2倍的数组,然后将原数组中的元素进行新一次散列,然后再插入到新数组中。

注意:ConcurrentHashMap不会对整个容器进行扩容,而是只对某个Segment进行扩容。

第二步:进行插入

将需要插入的元素通过散列定位,然后插入。(找准了,再插入)


ConcurrentHashMap的size()方法

size()方法就是来统计整个ConcurrentHashMap里元素的大小,也就是统计出每个segment的大小,然后求和。看似简单,但是细想又不是很简单,虽然,count是个volatile类型的全局变量,但是顶不住有其它线程在统计的时候捣乱呀。比如,当把所有的segment的大小求出准备进行相加时,又有其它线程对count进行了修改,这样统计的结果就不准确了。可是如果在统计过程中将其它方法锁住,呢岂不是效率太低了嘛?酸爽的体验感就有瑕疵了。

牛逼的大佬们给出的解决方案就是,先尝试2次不加锁进行统计,如果在统计过程中,count发生了变化,呢我再去加锁,再来进行统计。(有没有觉得很像自己,先裸爽,顶不住了在保护,既爽还持久,是不是很骚?)

那么是如何来感知count发生变化的呢?

使用了一个modCount变量,在put、remove和clean方法里操作元素前都会将变量modCount进行加1,那么在统计size 前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。

觉得有点CAS的味道。(个人感觉,感觉不对别喷我)


参考