jdk1.7源码下
ConcurrentHashMap的介绍
AbstractMap类继承了Map接口,也就是说存放着键值对,还有实现了Map接口那些方法。
通过“分段锁”来实现多线程下的安全问题。它将哈希表分成了不同的段内使用了可重入锁(ReentrantLock ),不同线程只在一个段内存在线程的竞争。它不会对整个哈希表加锁。
ConcurrentHashMap底层数据结构
ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构。它内部拥有一个HashEntry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)
ConcurrentHashMap有数组segment,segment有数组hashentry ,然后通过链表链接
如图所示:
Segment继承了可重入锁
HashEntry这个类有key、value、next、hash四个属性。HashEntry中的value被volatile修饰,这样在多线程读写过程中能够保持它们的可见性
HashMap、HashTable和ConcurrentHashMap区别是什么?
1.ConcurrentHashMap 、HashTable不允许值为null,值为null时抛出异常,HashMap允许值为null
2. HashTable所有方法都加锁synchronized,为线程安全的。HashMap不保证线程安全。ConcurrentHashMap采用了分段锁,在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。
3. ConcurrentHashMap为弱一致性,在get方法时有可能获得过时的数据。
在jdk1.7和1.8中的区别
数据结构的不同
1.7是对segment实行分段所 和自旋锁–数组+链表。
1.8是对实行了abs和syn原子操作—数组+链表+红黑树-- 优先红黑树
put、get方法
jdk1.7中
虽然 HashEntry 中的 value 是用 volatile 关键词修饰的,但是并不能保证并发的原子性,所以 put 操作时仍然需要加锁处理。
首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。
put的流程:
1.将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。
2.遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
3.不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
4.最后会解除在 1 中所获取当前 Segment 的锁。
get的流程:
只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。
由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。
ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。
jdk1.8中:
抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性
将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。
put的流程:
- 根据 key 计算出 hashcode 。
- 判断是否需要进行初始化。
- f 即为当前 key 定位出的Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
- 如果当前位置的 hashcode =MOVED=-1,则需要进行扩容。
- 如果都不满足,则利用 synchronized 锁写入数据。
- 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。
get的流程:
- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
- 如果是红黑树那就按照树的方式获取值。
- 就不满足那就按照链表的方式遍历获取值。