面试必问之——HashMap底层

本文详细探讨了HashMap、HashTable及CurrentHashMap的线程安全性、值处理、实现原理及优化策略。对比了HashMap与HashTable在null值处理、扩容机制及hash算法上的差异。深入分析了CurrentHashMap如何通过锁分段技术解决HashTable的效率瓶颈,以及HashMap在JDK1.8中引入红黑树以优化链表长度过长问题的原因。
摘要由CSDN通过智能技术生成

HashMap、HashTabe、CurrentHashMap

线程安全

HashMap线程不安全
HashTabe线程安全通过方法加锁来实现
CurrentHashMap线程安全,使用了分段锁来实现

HashTable不允许为null,key和value都不可以,HashTable直接使用对象的hashCode,hash数组默认大小为11,扩容方式为old * 2 + 1
HashMap允许null,key和value都可以,HashMap重新计算hash值,并且用于代替求模。hash数组默认大小为16,并且一定是2的指数,

HashMap的实现

JDK1.7 使用数组+链表实现(Entry)使用头插法

JDK1.8 使用数组+链表+红黑树实现(Node)使用尾插法

1.8使用尾插法的原因

头插法:新来的值会取代原有的值,原有的值就会顺推到链表中,(后来插入的值被获取到的可能性更大一些,提升查找效率)。

  • Infinite Loop

    • 单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置,在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,可能被放到新数组的不同位置上。一旦几线程调用完成,就可能出现环形链表。这时候去取值,悲剧就出现了(Infinite Loop)。
    • 造成原因就是扩容转移过程中修改了原来链表中节点的引用关系。
    • 使用尾插法不会引起死循环,是因为保持之前节点的引用关系。
      尾插法:Capacity:16(1<<4、16),LoadFactor:0.75f,
      使用16是为了算法均匀分布的原则,一个Key的hashcode的二进制&(length - 1)15的二进制结果为:Key的hashCode的最后几位。
      因为在使用不是2的幂的数字的时候,length - 1的值是所有二进制位全为1,这种情况下,index结果等同于HashCode后几位的值。
      只要输入的HashCode本事分布均匀、Hash算法的结果就是均匀的。
  • 在进行扩容的时候

    • resize:创建一个新的Entry数组,长度是原数组的2倍。
    • reHash:遍历原Entry数组,把所有的Entry重新Hash到新的数组,重新hash的公式为index = hashCode(Key) & (length - 1)
    • 使用头插会改变链表上的顺序,但是如果使用尾插,在扩容的时候会保持链表元素原本的顺序,就不会造成环的问题了。

虽然他不会造成死循环,但是通过put/get方法都没有同步锁,多线程情况最容易出现的就是:无法保证上一秒put的值,下一秒get的是原值,所以线程安全无法保障。

CurrentHashMap的实现

HashTable效率低下的原因是访问所有HashTable的线程必须竞争同一把锁。

所以CurrentHashMap使用了锁分段技术,容器里有多把锁,每一把锁用于锁容器里的一部分数据,当多线程访问容器里不同段的数据时,线程间就不会存在锁竞争,从而有效提高并发效率。当一个线程占用锁访问其中一个段的数据的时候,其他数据也能被其他线程访问。

对于get操作,一般不需要加锁,除非读到的值是空的时候才会加锁重读。

对于put操作,首先定位到对应的Segment,然后在Segment里进行插入操作,插入操作需要经历两个步骤:

  1. 判断是否需要对Segment里的HashEntry数组进行扩容
  2. 定位添加元素的位置然后放在HashEntry数组里

equals和hashCode

未重写equals方法我们是继承了Object的equals方法,那里的equals是比较两个对象的内存地址。
对于值对象,==比较两个对象的值
对于引用对象,比较的是两个对象的地址。

为什么要重写hashCode:需要对hashCode方法进行重写,以保证相同的对象返回相同的hash。
对于HashMap访问值的时候,根据key的hash找到index,假如index是链表的话,就需要重写equal方法来查找需要的值,这时候同样需要重写heshCode方法。避免对同一链表的对象无法分辨。

为什么Map桶中个数超过8才转为红黑树

这个问题是京东java面试官的问题?

通过查看源码

/**
 * The bin count threshold for using a tree rather than list for a
 * bin.  Bins are converted to trees when adding an element to a
 * bin with at least this many nodes. The value must be greater
 * than 2 and should be at least 8 to mesh with assumptions in
 * tree removal about conversion back to plain bins upon shrinkage.
 */
static final int TREEIFY_THRESHOLD = 8;

根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,小于等于6的时候就化为链表。

This map usually acts as a binned (bucketed) hash table, but
when bins get too large, they are transformed into bins of TreeNodes,
each structured similarly to those in java.util.TreeMap

TreeNodes占用空间是普通Nodes的两倍,所以只有当bin包含足够多的节点时才会转成TreeNodes,而是否足够多就是由TREEIFY_THRESHOLD的值决定的。当bin中节点数变少时,又会转成普通的bin。并且我们查看源码的时候发现,链表长度达到8就转成红黑树,当长度降到6就转成普通bin。

这样就解析了为什么不是一开始就将其转换为TreeNodes,而是需要一定节点数才转为TreeNodes,说白了就是trade-off,空间和时间的权衡。

Because TreeNodes are about twice the size of regular nodes, we
use them only when bins contain enough nodes to warrant use
(see TREEIFY_THRESHOLD). And when they become too small (due to
removal or resizing) they are converted back to plain bins.  In
usages with well-distributed user hashCodes, tree bins are
rarely used.  Ideally, under random hashCodes, the frequency of
nodes in bins follows a Poisson distribution
(http://en.wikipedia.org/wiki/Poisson_distribution) with a
parameter of about 0.5 on average for the default resizing
threshold of 0.75, although with a large variance because of
resizing granularity. Ignoring variance, the expected
occurrences of list size k are (exp(-0.5)*pow(0.5, k)/factorial(k)).
The first values are:
0:    0.60653066
1:    0.30326533
2:    0.07581633
3:    0.01263606
4:    0.00157952
5:    0.00015795
6:    0.00001316
7:    0.00000094
8:    0.00000006
more: less than 1 in ten million

当hashCode离散性很好的时候,树型bin用到的概率非常小,因为数据均匀分布在每个bin中,几乎不会有bin中链表长度会达到阈值。

但是在随机hashCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布。不过理想情况下随机hashCode算法下所有bin中节点的分布频率会遵循泊松分布,我们可以看到,一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。所以,之所以选择8,不是拍拍屁股决定的,而是根据概率统计决定的。由此可见,发展30年的Java每一项改动和优化都是非常严谨和科学的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值