面试官你能不能别问我 HashMap 了?-成都创新互联网站建设

关于创新互联

多方位宣传企业产品与服务 突出企业形象

公司简介 公司的服务 荣誉资质 新闻动态 联系我们

面试官你能不能别问我 HashMap 了?

本文转载自微信公众号「Java极客技术」,作者鸭血粉丝 。转载本文请联系Java极客技术公众号。

如果你是个 Java 程序员,那一定对 HashMap 不陌生,巧的是只要你去面试,大概率都会被问到 HashMap 的相关内容

那这篇文章你就一定要读一读了

HashMap 的底层数据结构

先来聊聊 HashMap 的底层数据结构 HashMap 的底层数据结构, 1.7 版本和 1.8 版本是有些不同的,但大体上都是 数组 + 链表 的形式来实现的, 1.7 版本是这个样子:

1.8 版本是这样:

很明显就能看出来, 1.8 版本怎么多了一个树?还是红黑的?

这就要来分析 1.7 版本 HashMap 的实现有什么不足了

1.7 版本主要就是 数组 + 链表,那么如果有一个 hash 值总是会发生碰撞,那么由此对应的链表结构也会越来越长,这个时候如果再想要进行查询操作,就会非常耗时,所以该如何优化这一点就是 1.8 版本想要实现的

1.8 版本采用了 数组 + 链表 + 红黑树 的方式去实现,当链表的长度大于 8 时,就会将链表转为红黑树。

这个时候问题就来了,为什么会将链表转红黑树的值设定为 8 ?

因为链表的时间复杂度是 n/2 ,红黑树时间复杂度是 logn ,当 n 等于 8 的时候, log8 要比 8/2 小,这个时候红黑树的查找速度会更快一些

为什么是小于 6 的时候转为链表,而不是 7 的时候就转为链表呢?频繁的从链表转到红黑树,再从红黑树转到链表,开销会很大,特别是频繁的从链表转到红黑树时,需要旋转

为什么将链表转为红黑树,而不是平衡二叉树( AVL 树)呢?

  • 因为 AVL 树比红黑树保持着更加严格的平衡, AVL 树中从根到最深叶的路径最多为 1.44lg(n + 2) ,红黑树中则最多为 2lg( n + 1) ,所以 AVL 树查找效果会比较快,如果是查找密集型任务使用 AVL 树比较好,相反插入密集型任务,使用红黑树效果就比较 nice
  • AVL 树在每个节点上都会存储平衡因子
  • AVL 树的旋转比红黑树的旋转更加难以平衡和调试,如果两个都给 O(lgn) 查找, AVL 树可能需要 O(log n) 旋转,而红黑树最多需要两次旋转使其达到平衡

HashMap 为什么是线程不安全的?

HashMap 的线程不安全主要体现在两个方面:扩容时导致的死循环 & 数据覆盖

扩容时导致的死循环,这个问题只会在 1.7 版本及以前出现,因为在 1.7 版本及以前,扩容时的实现,采用的是头插法,这样就会导致循环链表的问题

什么时候会触发扩容呢?如果存储的数据,大于 当前的 HashMap 长度( Capacity ) * 负载因子( LoadFactor ,默认为 0.75) 时,就会发生扩容。比如当前容量是 16 , 16 * 0.75 = 12 ,当存储第 13 个元素时,经过判断发现需要进行扩容,那么这个时候 HashMap 就会先进行扩容的操作

扩容也不是简简单单的将原来的容量扩大就完事儿了,扩容时,首先创建一个新的 Entry 空数组,长度是原数组的 2 倍,扩容完毕之后还会再进行 ReHash ,也就是将原 Entry 数组里面的数据,重新 hash 到新数组里面去

假设现在有一个 Entry 数组,大小是 2 ,那么当我们插入第 2 个元素时,大于 2 * 0.75 那么此时就会发生扩容,具体如下图:

扩容完毕之后,因为采用的是头插法,所以后面的元素会放在头部位置,那么就可能会这样:

刚开始记录的是 A.next = B ,经过扩容之后是 B.next = A ,那么最后可能就是这样了:

明显看到造成了死循环,比较好的是, 1.8 版本之后采用了尾插法,解决了这个问题

还有个问题, 1.8 版本是没有解决的,那就是数据覆盖问题

假设现在线程 A 和线程 B 同时进行 put 操作,特别巧的是这两条不同的数据 hash 值一样,并且这个位置数据为 null ,那么是不是应该让线程 A 和 B 都执行 put 操作。假设线程 A 在要进行插入数据时被挂起,然后线程 B 正常执行将数据插入了,然后线程 A 获得了 CPU 时间片,也开始进行数据插入操作,那么就将线程 B 的数据给覆盖掉了

因为 HashMap 对 put 操作没有进行加锁的操作,那么就不能保证下一个线程 get 到的值,就一定是没有被修改过的值,所以 HashMap 是不安全的

那既然 HashMap 线程不安全,你给推荐一个安全的?

如果推荐的话,那肯定推荐 ConcurrentHashMap ,说到 ConcurrentHashMap 也有一个比较有趣的事情,那就是 ConcurrentHashMap 的 1.7 版本和 1.8 版本实现也是不一样

在 1.7 版本, ConcurrentHashMap 采用的是分段锁( ReentrantLock + Segment + HashEntry )实现,也就是将一个 HashMap 分成多个段,然后每一段都分配一把锁,这样去支持多线程环境下的访问。但是这样锁的粒度太大了,因为你锁的直接就是一段嘛

所以 1.8 版本又做了优化,使用 CAS + synchronized + Node + 红黑树 来实现,这样就将锁的粒度降低了,同时使用 synchronized 来加锁,相比于 ReentrantLock 来说,会节省比较多的内存空间

HashMap 这块,其实还可以扩展,比如 HashMap 和 HashTable 的区别, ConcurrentHashMap 1.7 版本和 1.8 版本具体的实现,等等等等

但是这篇文章已经比较长了,就写到这里吧~


当前名称:面试官你能不能别问我 HashMap 了?
URL分享:http://kswsj.cn/article/ccchidp.html

其他资讯