One minute
ConcurrentHashMap探秘
它是HashMap
的多线程版本,是线程安全的。可以在并发环境下直接使用。为什么放着好好的HashTable
不用又要从新实现一个ConcurrentHashMap
呢?就是因为效率问题(虽然你安全,但是太慢了,没有爽感。)这个就不一样,即安全,效率还高,这酸爽你能顶的住?
ConcurrentHashMap
是将数据分成一段一段存储,然后给每个数据段配一把锁,当一个线程访问占用启用一个数据段的同时,其它数据段也能被其他线程访问。(这种思想就类比公共厕所里的一排蹲坑就行。)
注意:
ConcurrentHashMap
读不加锁,下文会有解释。
ConcurrentHashMap之内部数据结构
JDK1.7:
ConcurrentHashMap
是由segment
数组结构和HashEntry
组成,segment
实现了ReentrantLock
,所以扮演了一个可重入锁的角色。HashEntry
用于存储键值对数据。
一个 ConcurrentHashMap
里包含一个 Segment
数组。Segment
的结构和HashMap
类似,是一种数组和链表结构,一个 Segment
包含一个 HashEntry
数组,每个 HashEntry
是一个链表结构的元素,每个 Segment
守护着一个HashEntry
数组里的元素,当对 HashEntry
数组的数据进行修改时,必须首先获得对应的 Segment
的锁。
注意:初始化时容器里面默认的锁的个数,也就是
Segment
数组长度为16;具体解释可以看《Java并发编程艺术》P279图片出处见参考
JDK1.8:
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的味道。(个人感觉,感觉不对别喷我)
参考
- 《Java并发编程的艺术》
- 大佬博客以及图片出处