hashmap面试必问。所以一定要弄明白
首先介绍hashmap
jdk1.7hashmap
底层采用 数据 + 链表 比较简单。相信很多人都自己实现过一遍。采用头插法插入数据。这种方式不安全
下图
重点来了:
当数组扩容的时候,我们采用的方法是这样的,扩容当然是在原有的基础 * 2 ,
这里暂时记下当前容量为 capacity 。两倍就相当于原来的capacity向左移动一位,但还是 111111这种形式的。
我们计算index 值是 key.hash & capacity - 1 ,所以我们只需要在key.hash 最新增加的那位吧,(什么叫做最新增加的那位,额,之前我们对应的length 二进制不是 1111吗,* 2 之后就会变成 11111,然后对应的key.hash不也就多加了一位吗)
如果这位为1,新的index = capacity + oldIndex , 如果为0 , index = oldIndex;
接下来我们要了解的是,为什么头插法不安全。
头插法会导致链表死循环。你还听不懂的话看图吧
原始链表:
容量翻倍 变成4,B这个时候算的是index = 2.
C的index也等于2 然后你看一下,是不是死循环了。
JDK1.8 hashMap
hashmap采用的是 数据 + 链表 + 红黑数。更加高
当链表上的元素超过8,就转成红黑树存储。
(h = k.hashCode()) ^ (h >>> 16) //采用高16为异或低16位,这样尽量让所有的数据都参与到运算来。更加分散吧。
h & length - 1 // 下标
然后和jdk1.7是一样的找下标。
重点: hashMap采用的是尾插法避免出现环链表。
jdk1.7 ConCurrentHashMap
Segment + HashEntry + ReentrantLock
使用分段锁,segment[] 每一个数组坑位加一把锁。 也就是对Node数组添加volatile关键字。
Segment这个类继承了重入锁ReentrantLock
怎么扩容Segment这个类继承了重入锁ReentrantLock?
段(segment)+数组+链表。扩容操作是先遍历数组元素,在每个数组元素上遍历一遍链表,找到链表的最后n个结点(这n个结点在新数组一定属于同一个数组位置上),把这n个结点先挂接到新数组的数组位置上,这也叫lastRun机制。至于原数组的头结点到倒数n个结点之间的结点,再遍历一遍,通过复制每个结点的机制挂接到新数组上。
他到底是怎么扩容的 你能不能说清楚一点。认真研读下面文字。
它是指使用lastRun标识,标志那些在后面有一串都是0的结点,因为他们都是留在原位置的 lastRun标记位置开始到末尾,那个新增的数是0;当然扩容是换了一个长度* 2 的新数组进行操作。 直接就可以提到新数组中去。然后剩下的就是直接遍历了,一个一个来。莫得办法。
jdk1.8ConCurrentHashMap
底层使用: 数组+链表+红黑树+数据加锁。也就是在1.8 hashmap上面进行操作。
锁的粒度更加细。
采用多线程去扩容。只是内部操作很多属性都是加了 volatile关键字
首先将扩容线程分成两半,每段线程独立操作,每一半里面又会分成两半。(条件:不能再分,线程达到达到最大)
每一个线程可以将链表中的数据拆分成两种
1、新增位为0 , 插入oldIndex
2、新增位为1,插入oldIndex + capacity