普通人
嗯.. ConcurrentHash Map 是用数组和链表的方式来实现的,嗯 … 在 JDK1.8 里面还引入了红黑树。
然后链表和红黑树是解决 hash 冲突的。嗯 … …
高手
这个问题我从这三个方面来回答:(下面这三个点 ,打印在屏幕上)
1. ConcurrentHash Map 的整体架构
2. ConcurrentHash Map 的基本功能
3. ConcurrentHash Map 在性能方面的优化
ConcurrentHash Map 的整体架构
这个是 ConcurrentHash Map 在 JDK1.8 中的存储结构 ,它是由数组、单向链表、红黑树组成。
当我们初始化一个 ConcurrentHash Map 实例时,默认会初始化一个长度为 16 的数组。
由于 ConcurrentHash Map 它的核心仍然是 hash 表,所以必然会存在 hash 冲突问题。
ConcurrentHash Map 采用链式寻址法来解决 hash 冲突。
当 hash 冲突比较多的时候 ,会造成链表长度较长 ,这种情况会使得
ConcurrentHash Map 中数据元素的查询复杂度变成 O(n)。因此在 JDK1.8 中,引入了
红黑树的机制。
当数组长度大于 64 并且链表长度大于等于 8 的时候 ,单项链表就会转换为红黑树。
另外 ,随着 ConcurrentHash Map 的动态扩容 ,一旦链表长度小于 8 ,红黑树会退化
成单向链表。
ConcurrentHash Map 的基本功能
ConcurrentHash Map 本质上是一个 Hash Map ,因此功能和 Hash Map 一样 ,但是
ConcurrentHash Map 在 Hash Map 的基础上 ,提供了并发安全的实现。
并发安全的主要实现是通过对指定的 Node 节点加锁,来保证数据更新的安全性(如图所示)。
ConcurrentHash Map 在性能方面做的优化
如果在并发性能和数据安全性之间做好平衡 ,在很多地方都有类似的设计 ,比如 cpu
的三级缓存、mysql 的 buffer_pool、Synchronized 的锁升级等等。
ConcurrentHash Map 也做了类似的优化 ,主要体现在以下几个方面:o 在 JDK1.8 中 ,ConcurrentHash Map 锁的粒度是数组中的某一个节点 ,而在 JDK1.7 ,锁定的是 Segment ,锁的范围要更大 ,因此性能上会更低。
引入红黑树 ,降低了数据查询的时间复杂度 ,红黑树的时间复杂度是 O(logn)。
(如图所示),当数组长度不够时,ConcurrentHash Map 需要对数组进行扩 容 ,在扩容的实现上 ,ConcurrentHash Map 引入了多线程并发扩容的机制, 简单来说就是多个线程对原始数组进行分片后,每个线程负责一个分片的数据 迁移 ,从而提升了扩容过程中数据迁移的效率。
ConcurrentHash Map 中有一个 size()方法来获取总的元素个数 ,而在多线程 并发场景中,在保证原子性的前提下来实现元素个数的累加,性能是非常低的。
ConcurrentHash Map 在这个方面的优化主要体现在两个点:
当线程竞争不激烈时 ,直接采用 CAS 来实现元素个数的原子递增。
如果线程竞争激烈 ,使用一个数组来维护元素个数 ,如果要增加总的元素 个数,则直接从数组中随机选择一个,再通过 CAS 实现原子递增。它的核心思想是引入了数组来实现对并发更新的负载。
结尾
从高手的回答中可以看到 ,ConcurrentHash Map 里面有很多设计思想值得学习和借鉴。
比如锁粒度控制、分段锁的设计等 ,它们都可以应用在实际业务场景中。
很多时候大家会认为这种面试题毫无价值,当你有足够的积累之后,你会发现从这些技
术底层的设计思想中能够获得很多设计思路。