一、为什么要用ConcurrentHashMap?
1、HashMap线程不安全,并且进行put操作会导致死循环(由于HashMap的Entry链表形成环形数据结构,Entry下的next节点永远不为空)
2、HashTable多线程效率低下,主要表现在数据操作方法头采用synchronized互斥锁,使得其他线程访问同步方法会进入阻塞或者是轮询状态(就是线程会无限循环查看锁是否被释放)
3、ConcurrentHashMap的锁分段技术可提升并发访问效率(表现在吞吐量)
二、结构
解析:Segment是一种可重入锁,在ConcurrentHashMap中扮演锁的角色,HashEntry则是用于存储键值对数据。ConcurrentHashMap与Segment和HashEntry是一一对应的,Segment对HashEntry充当锁的角色,每当对HashEntry数组的数据进行修改时,首先要获取对应的Segment锁
解决散列冲突的方式是采用分离链表法:
分散链表法使用链表解决冲突,将散列值相同的元素都保存到一个链表中。当查询的时候,首先找到元素所在的链表,然后遍历链表查找对应的元素,也就是会根据key定位到链表的某个位置,所以不管是hashMap还是concurrentHashMap相同key是只取最后插入的值
图片来自 http://faculty.cs.niu.edu/~fr...
三、初始化
capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍,默认为16,即默认允许16个线程同时访问。
loadFactor:负载因子,默认为 0.75。
threshold:扩容的阈值,等于 capacity * loadFactor
初始化Segment数组,该长度ssize是通过concurrencyLevel计算得出,需要能通过按位与的散列算法来定位数组的索引,故必须要保证Segment的长度为2的N次方,故concurrencyLevel为14、15、16,ssize都等于15,即容器锁的格式也是16
初始化segment,initialCapacity为ConcurrentHashMap初始化容量,loadfactor是每个segment的负载因子
四、定位Segment
由于ConcurrentHashMap使用分段锁Segment来保护不同段的数据,那么在插入和读取元素的时候,必须先通过Wang/Jenkins hash的变种算法对元素的hashCode进行再次散列
那么为什么需要再次散列呢?
为了减少散列冲突,使得元素能够均匀分布在不同的segment中,从而达到提高容器的存取效率,那么不再次散列会出现什么结果?
会导致不同数值它的散列值可能会相同,从而导致所有数值都存在同一个segment中,从而导致segment失去分段锁的意义并且存取缓慢
如果进行再次散列,那么所有的数值都会散列开,之前的问题迎刃而解
五、get、put、size操作
ConcurrentHashMap高效的原因之一在于get操作不需要加锁,因为它所有共享变量都定义成volatile,能够保证在多线程之间的可见性,能够被多线程同时读,并且保证不会读到过期值,并且由volatile修饰的共享变量均不需要回写主内存
put操作时由于对共享变量进行了写的操作,故必须加锁,那么put方法首先会定位到segment(因为segment相当于装载元素的桶),然后进行插入操作,操作分为2步:
1、在插入元素之前会先判断是否需要扩容(所有容器都是需要判断扩容),做法是如果capacity *loadFactor大于threshold则扩容,
2、扩容多少呢?首先会创建一个容量为原来2倍的数组,将原有元素进行散列后插入新的数组,为了高效,只会针对某个segment进行扩容
那这个就是负载因子的作用,这个操作和HashMap不一致,HashMap是在插入完之后进行判断是否需要扩容,就会导致如果下次没有插入数据,那么这个空间就浪费了,在jdk1.8还做了部分修改
size操作:就是为了统计segment的所有大小,那么segment的count是个全局变量是用volatile修饰,最快速的做法是,把所有的segment的count加起来,但是如果在这期间出现了修改操作,那么count的统计就不精确了,如果把segment的put、remove、clean全部锁住,那么效率太低,目前的做法是,尝试2次不锁住segment来统计count,如果统计过程中出现容器变化,那么就加锁,来统计,ConcurrentHashMap是如何知道统计过程中容器发生了变化,用modCount变量,在put、remove、clean方法里操作元素前会将modCount进行加1,比较前后变化
jdk1.8不再采用segment作为锁的粒度,而是直接将HashEntry作为锁,从而降低锁的粒度、
jdk1.8使用内置锁synchronized来代替重入锁ReentrantLock
因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存,虽然不是瓶颈,但是也是一个选择依据
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。