ConcurrentHashMap原理是面试的一个高频知识点,这属于第一阶段的过招。
如果你答上来了,之后面试官可能会顺势追问一下,如何提高 ConcurrentHashMap 的插入效率?于是一个接着一个,连环问就开始了,直到探到你的老底才罢休。
今天我们用一篇文章把ConcurrentHashMap讲清楚,帮你在面试时不慌不忙,从容应对。
1、ConcurrentHashMap 原理概述
ConcurrentHashMap 是一个存储 key/value 对的容器,并且是线程安全的。我们先看 ConcurrentHashMap 的存储结构,如下图:
这是经典的数组加链表的形式。并且在链表长度过长时转化为红黑树存储( Java 8 的优化),加快查找速度。
存储结构定义了容器的 “形状”,那容器内的东西按照什么规则来放呢?换句话讲,某个 key 是按照什么逻辑放入容器的对应位置呢?
我们假设要存入的 key 为对象 x,这个过程如下:
-
通过对象 x 的 hashCode () 方法获取其 hashCode;
-
将 hashCode 映射到数组的某个位置上;
-
把该元素存储到该位置的链表中。
从容器取数的逻辑如下:
-
通过对象 x 的 hashCode () 方法获取其 hashCode;
-
将 hashCode 映射到数组的某个位置上;
-
遍历该位置的链表或者从红黑树中找到匹配的对象返回。
这个数组 + 链表的存储结构其实是一个哈希表。
把对象 x 映射到数组的某个位置的函数,叫做 hash 函数。
这个函数的好坏决定元素在哈希表中分布是否均匀,如果元素都堆积在一个位置上,那么在取值时需要遍历很长的链表。
但元素如果是均匀的分布在数组中,那么链表就会较短,通过哈希函数定位位置后,能够快速找到对应的元素。
具体 ConcurrentHashMap 中的哈希函数如何实现我们后面会详细讲到。
扩容
我们大致了解了 ConcurrentHashMap 的存储结构。
那么我们思考一个问题,当数组中保存的链表越来越多,那么再存储进来的元素大概率会插入到现有的链表中,而不是使用数组中剩下的空位。
这样会造成数组中保存的链表越来越长,由此导致哈希表查找速度下降,从 O (1) 慢慢趋近于链表的时间复杂度 O (n/2),这显然违背了哈希表的初衷。
所以 ConcurrentHashMap 会做一个操作,称为扩容。
也就是把数组长度变大,增加更多的空位出来,最终目的就是预防链表过长,这样查找的时间复杂度才会趋向于 O (1)。
扩容的操作并不会在数组没有空位时才进行,因为在桶位快满时,新保存元素更大的概率会命中已经使用的位置,那么可能最后几个桶位很难被使用,而链表却越来越长了。ConcurrentHashMap 会在更合适的时机进行扩容,通常是在数组中 75% 的位置被使用时。