目录
ConcurrentHashMap是如何保证线程安全的?底层是如何实现的?
JDK1.8ConcurrentHashMap是如何实现线程安全的?
ConcurrentHashMap,它是HashMap高并发的版本,线程安全。
hashmap在并发操作时会出现各种问题,比如死循环问题、数据覆盖[??]等问题,使用ConcurrentHashMap就可以进行解决。
在jdk1.7中,它采用数组+链表的方式进行实现,而数组又分为大数组Segment和小数组HashEntry,【大数组Segment【部分,分割】可以理解为Mysql中的数据库,而每个数据库(Segment)中又有很多张表HashEntry,每个HashEntry中又有很多条数据,这些数据是采用链表的方式进行连接的,然后加锁是通过给Segment添加ReentrantLock可重入锁来实现线程安全的,如图
而JDK1.8过后,采用了数组+链表+红黑树的方式优化了ConcurrentHashMap的实现,具体实现方式为才链表长度大于8,并且数组长度大于64时,链表就会升级成红黑树的结构(查询效率就会更高)
当调用put方法时,会使用的CAS+Volatile【不稳定的】或synchronized的方式来保证线程安全的,核心源码如下,
- public V put(K key, V value) {
- return putVal(key, value, false);
- }
-
- /** Implementation for put and putIfAbsent */
- final V putVal(K key, V value, boolean onlyIfAbsent) {
- if (key == null || value == null) throw new NullPointerException();
- int hash = spread(key.hashCode());
- int binCount = 0;
- for (Node
[] tab = table;;) { - Node
f; int n, i, fh; - if (tab == null || (n = tab.length) == 0)
- tab = initTable();
- else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
- if (casTabAt(tab, i, null,
- new Node
(hash, key, value, null))) - break; // no lock when adding to empty bin
- }
- else if ((fh = f.hash) == MOVED)
- tab = helpTransfer(tab, f);
- else {
- V oldVal = null;
- synchronized (f) {
- if (tabAt(tab, i) == f) {
- if (fh >= 0) {
- binCount = 1;
- for (Node
e = f;; ++binCount) { - K ek;
- if (e.hash == hash &&
- ((ek = e.key) == key ||
- (ek != null && key.equals(ek)))) {
- oldVal = e.val;
- if (!onlyIfAbsent)
- e.val = value;
- break;
- }
- Node
pred = e; - if ((e = e.next) == null) {
- pred.next = new Node
(hash, key, - value, null);
- break;
- }
- }
- }
- else if (f instanceof TreeBin) {
- Node
p; - binCount = 2;
- if ((p = ((TreeBin
)f).putTreeVal(hash, key, - value)) != null) {
- oldVal = p.val;
- if (!onlyIfAbsent)
- p.val = value;
- }
- }
- }
- }
- if (binCount != 0) {
- if (binCount >= TREEIFY_THRESHOLD)
- treeifyBin(tab, i);
- if (oldVal != null)
- return oldVal;
- break;
- }
- }
- }
- addCount(1L, binCount);
- return null;
- }
从源码可以看出,添加元素首先会判断容器是否为空,如果为空则使用volatile加CAS来初始化
如果容器不为空,则根据需要存储的元素计算出在容器该位置中是否为空,如果为空,则利用【乐观锁】CAS设置该节点,如果不为空则使用synchronized加锁,遍历桶table中的数据,替换或新增节点到桶中,最后判断是否需要转换成红黑树【判断链表长度和数组长度】,这样就能保证并发的访问时,线程是安全的了。
ConcurrentHashMap是在头节点加锁保证线程安全的,锁的粒度【就是锁的代码更少了】相比segment来说更小了,发送锁冲突和加锁的频率降低了。并发操作时,执行效率就更高了。
JDK1.8使用的红黑树结构优化了之前的链表结构,当数据量比较大的时候,查询效率就得到了很大的提升,时间复杂度从之前的O(n)优化到了O(logn)