目录
一、ConcurrentHashMap与HashMap等的区别
二、JDK1.7版本的CurrentHashMap的实现原理
三、JDK1.8版本的CurrentHashMap的实现原理
由于 ConcurrentHashMap 的实现在JDK1.7 和 JDK1.8 有较大不同,因此会分开两个版本讲解。
一、ConcurrentHashMap与HashMap等的区别
1.HashMap
我们知道HashMap是线程不安全的,在多线程环境下,使用Hashmap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。
2.HashTable
HashTable和HashMap的实现原理几乎一样,差别无非是
HashTable不允许key和value为nullHashTable是线程安全的但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁。
多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。
3.ConcurrentHashMap
主要就是为了应对hashmap在并发环境下不安全而诞生的,ConcurrentHashMap的设计与实现非常精巧,大量的利用了volatile,final,CAS等lock-free技术来减少锁竞争对于性能的影响。
我们都知道Map一般都是数组+链表结构(JDK1.8改为数组+红黑树)。
ConcurrentHashMap避免了对全局加锁改成了局部加锁操作,这样就极大地提高了并发环境下的操作速度,由于ConcurrentHashMap在JDK1.7和1.8中的实现非常不同,接下来我们谈谈JDK在1.7和1.8中的区别
二、JDK1.7版本的CurrentHashMap的实现原理
在JDK1.7中ConcurrentHashMap采用了数组+Segment+分段锁的方式实现。
1.Segment(分段锁)
ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
2.内部结构
ConcurrentHashMap使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。如下图是ConcurrentHashMap的内部结构图:
上图中的,HashEntry跟HashMap差不多的,但是不同点是,他使用volatile去修饰了他的数据Value还有下一个节点next。这给get读数据时可以不加锁埋下了伏笔。
transient volatile HashEntry<K,V>[] table;
从上面的结构我们可以了解到,ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作。
第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部。
3.该结构的优劣势
坏处
这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长
好处
写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment,这样,在最理想的情况下,ConcurrentHashMap可以最高同时支持Segment数量大小的写操作(刚好这些写操作都非常平均地分布在所有的Segment上)。
所以,通过这一种结构,ConcurrentHashMap的并发能力可以大大的提高。
三、JDK1.8版本的CurrentHashMap的实现原理
JDK1.8中ConcurrentHashMap参考了JDK1.8 HashMap的实现,采用了数组+链表+红黑树的实现方式来设计,内部采用了CAS + synchronized 来保证并发更新的安全。
1.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但依然存在 HashMap 在 1.7 版本中的问题:那就是查询遍历链表的效率太低。
因此参考了JDK1.8中HashMap的改变。
在JDK1.8的版本中,ConcurrentHashMap抛弃了分段锁(Segment),这时候ConcurrentHashMap的结构跟HashMap差不多,都引入了红黑树,使得ConcurrentHashMap的实现非常复杂,我们都知道,红黑树是一种性能非常好的二叉查找树,其查找性能为O(logN),早期完全采用链表结构时Map的查找时间复杂度为O(N),JDK8中ConcurrentHashMap在链表的长度大于某个阈值的时候会将链表转换成红黑树进一步提高其查找性能。
ConcurrentHashMap具体如下图:
CurrentHashMap和HashTable的区别:
HashTable是每次操作都是锁住整个哈希表的,效率非常低下。一个线程访问同步方法时,当其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程就不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈,效率就越低。
而CurrentHashMap,是当代码访问相应的bucket时,使用sychronizeded关键字,防止多个线程同时操作同一个bucket。
get操作
get操作是可以不加锁的。
使用 volatile 来保证某个变量内存对其他线程即时可见在配合 CAS 可以实现 不加锁对并发操作的支持。
get操作可以无锁(不加锁)是由于 Node 元素 的val 指针 和 next指针 用volatile修饰的,在多线程环境下,线程A修改结点的 val 或者新增结点的时候,对线程B都是可见的。
put操作
当线程访问哈希表的bucket时,使用 sychronizeded关键字,防止多个线程同时操作同一个 bucket(即锁住bucket),如果该结点的 hash值不少于0,则遍历链表更新节点或插入新节点;如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;更新了节点数量,还要考虑扩容和链表转红黑树。