除了Map系列应该有的线程安全的get,put等方法外,ConcurrentHashMap还提供了一个在并发下比较有用的方法 putIfAbsent.
如果传入key对应的value已经存在,就返回存在的value,不进行替换。如果不存在,就添加key和value,返回null。
V
JDK1.7的ConcurrentHashMap实现原理
1、底层:
(1)底层数据结构:
<jdk1.7>:数组(Segment) + 数组(HashEntry) + 链表(HashEntry节点)
底层一个Segments数组,存储一个Segments对象,一个Segments中储存一个Entry数组,存储的每个Entry对象又是一个链表头结点。
![3dcea38fec5df1a2a8df0d107ca54d3b.png](https://i-blog.csdnimg.cn/blog_migrate/6a8ce0f88b01d84bc2320ef0a0e9e487.jpeg)
(2)基本属性:
两个主要的内部类:
class Segment内部类,继承ReentrantLock,有一个HashEntry数组,用来存储链表头结点
public
(3)主要方法:
get():
1、第一次哈希 找到 对应的Segment段,
调用Segment中的get方法
2、再次哈希找到对应的链表,
3、最后在链表中查找。
public
此方法没有加锁,但是HashEntry结构中的value前面又volatile关键字来保证可见性、有序性。但不保证原子性。
put():
1、首先确定段的位置,然后调用Segment中的put方法:
2、加锁
3、检查当前Segment数组中包含的HashEntry节点的个数,如果超过阈值就重新hash
4、然后再次hash确定放的链表。
5、在对应的链表中查找是否相同节点,如果有直接覆盖,如果没有将其放置链表尾部
public
size():
1、首先对segments进行两次统计操作,如果两次的统计值相同,则说明这段时间没有线程来访问这个map,拿到的size大小是正确的,返回。
2、如果前两次拿到的值不相同,第三次就先对每一个segment进行加锁,然后在整体遍历计算一次,最后释放锁。所以在并发度比较高的情况之下,不建议使用这个方法,会把所有的segment都加锁。类似的还有containsValue(Object value)这个方法,如果我们只需要判断当前map不为空,可以用isEmpty()方法,它是不加锁的。
public
ConcurrentHashMap():
public
这里面两点要说明的,在上面代码的注释中也有提到:
初始化大小一定是2的指数级
初始化segments数组的时候只会对segments[0]进行初始化,其他结点的初始化是在put操作的时候。
4) 重哈希方式 :重点:
重哈希的方式 :只是对 Segments对象中的HashEntry数组进行重哈希,ConcurrentHashMap初始化之后就不可更改segments数组大小,默认的初始化值为16,那么它的并发度也是16,如果初始化时给segments定义的太大,也会造成命中的效率下降,影响整体的性能,一般默认的也够用了。
2、通过什么保证线程安全
分段锁 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
JDK1.8的ConcurrentHashMap实现原理
1、底层:
(1)底层数据结构:
<jdk1.8>:数组(Segment + 链表/红黑树,类似于hashMap<jdk1.8>
Node数组使用来存放树或者链表的头结点,当一个链表中的数量到达一个数目时(8),会使查询速率降低,所以到达一定阈值时,会将一个链表转换为一个红黑二叉树,提高查询的速率。
![8998924ca21ea94c4e355ef9fff192f9.png](https://i-blog.csdnimg.cn/blog_migrate/b937a32614499fb3a4909c377c5f0b66.jpeg)
(2)基本属性:
public
(3)主要方法:
ConcurrentHashMap():
public
初始化不做任何操作,也不会初始化table数组,在调用put()方法的时候判断table[]如果为null,进行初始化。
put():
public
get():
public
size():
public
并发下的Map常见面试题汇总:
HashMap 和 HashTable 有什么区别?
HashMap 线程不安全,HashTable 线程安全(每个方法前面有一个synchronized),前者效率高一些
HashMap 是允许key或者value为null,HashTable 不允许,ConcurrenotHashMap也是不允许的
HashMap 默认初始化大小为16,haHashTable为11
扩容时,HashMap缺省时扩大两倍,而HashMap 扩大两倍+1
HashMap散列时会重新计算hash,HashTable 直接使用它的hashCode值
Java 中的另一个线程安全的与 HashMap 极其类似的类是什么?同样是线程安全,它与 HashTable 在线程同步上有什么不同?
与 HashMap 极其类似的类是ConcurrentHashMap
HashTable是锁整张表,在高并发的情况下效率低一些,ConcurrentHashMap 1.7采用分段锁,1.8中采用cas+synchronized +分段锁大大提升了并发效率。
HashTable是强一致,在get时直接锁整张表,而ConcurrentHashMap 在get时是不加锁的,可能在拿的过程中数据被改,是弱一致的。
HashMap & ConcurrentHashMap 的区别?
前者线程不安全,后者线程安全
前者允许key or value值为空,后者不允许key or value值为空
在1.8中他们的红黑树TreeNode继承不同,HashMap的TreeNode继承自LinkedHashMap.Entry,而ConcurrentHashMap 的TreeNode继承自Node
ConcurrentHashMap 锁机制具体分析(JDK 1.7 VS JDK 1.8)?
1.7中底层采用数组+数组+链表结构,使用Segment和HashEntry类,而Segment继承自可重入的ReentrantLock同步锁来充当锁的角色,由每个segment保护自己所属的数据,而HashEntry所组成的一个数组又在segment下面
1.8采用node+cas+synchronized
ConcurrentHashMap 在 JDK 1.8 中,为什么要使用内置锁 synchronized 来代替重入锁 ReentrantLock?
1.开发团队在jdk1.8上对synchronized 做了大量的性能优化,而且基于虚拟机本来的关键字优化空间更大,更加自然;
2.显示锁是一个对象,要消耗内存,而synchronized 作为一个语言特性,它的内存消耗会更少
1.8下ConcurrentHashMap 简单介绍?
数据结构;
1.重要成员变量;sizeCtrl,控制初始化大小以及扩容大小,它也表示当前是否正在扩容
2.重要数据结构:node:table[]中的基本数据结构
treeNode, treeBin:为了支持红黑树,扩展出来的继承Node
方法:
put:如果没有初始化,先进性初始化,直接插入用cas,如需要扩容,先进行扩容,如果hash冲突,对当前node结点进行加锁,链表以尾插法的形式插入,红黑树按照红黑树的方式插入,如果链表长度大于8,转红黑树,如果size>sizeCtrl,则进行扩容。
get:分别按照链表或者红黑树的结构去查找数据
并发扩容,每个线程以步长的形式对结点进行扩容。
ConcurrentHashMap 的并发度是什么?
1.7中同时更新ConcurrentHashMap,并且不产生锁竞争的最大线程并发数,默认为16,而且可以在构造函数中设置,但是 ConcurrentHashMap会使用>=当前设置值的最小2^n作为实际的并发度。
1.8中并发度则无实际的用处,若初始容量小于并发度,将初始容量提升至并发度大小。