本篇用来整理在网上学习到的关于HashMap的一些知识点。
目录
1.7中HashMap为 数组+链表;1.8中增加了 红黑树,当数组长度大于64,链表长度超过8(不包括)使用红黑树。(为什么是超过8变成红黑树?那什么时候会从红黑树退化成链表呢?)
关于初始容量
初始容量的默认大小为16,Map的容量永远会是2的次幂(为什么是2的次幂呢?待会儿解释),Map的容量是可以手动进行设置的,你甚至可以设置为0(但是不能为负数),设置为7的话,Map会自动变为大于7的一个2的次幂,也就是8。
负载因子:0.75f(也是可以自定义设置的;为什么有负载因子的存在?为什么负载因子就是0.75?待会儿解释)
例如容量为16的时候,当你要放入的元素是第 16*0.75f = 12 个元素时,Map便会扩容;
HashMap只有在第一次put的时候才会创建,懒加载机制;
根据要存的数据量去设置初始容量是一个好习惯,假如我们要装80个元素的时候,128大于80,128*0.75f也大于80,容量可以直接设置为128,减少存数据时HashMap扩容的次数。这只是举例,数据量小的时候提现不出来,在数据量很大是时候,效率会有明显提升。阿里巴巴开发手册中建议定义集合的容量,而不是置空。
为什么有负载因子?为什么是3/4?
假如有一个HashMap的容量现在为16,没有负载因子的情况下(也可以理解负载因子为1),当数据存到第16个数据的时候,HashMap才会进行扩容,假设我们要存入第15个数据,此时容量为16,已经放入了14个数据,那么此时的哈希冲突概率为14/16,HashMap要求低哈希冲突,这样显然是不符合的;所以,需要有负载因子的存在,去降低哈希冲突。负载因子的值为什么是0.75f呢?
上述可得,负载因子越大,哈希冲突的概率就越大,但是扩容次数会减少,反之负载因子越小,哈希冲突的概率就越小,但是扩容次数会因此增加;0.75f是在前辈们不断的测试中得出的一个折中(降低hash冲突的同时尽可能提升空间使用率)的数字。
在HashMap源码中,判断是否需要扩容,需要一行代码:oldTab.length * 3/4
oldTab就是执行resize之前的table,也就是扩容前的HashMap,由于HashMap会经常扩容,所以这行代码会经常执行;table.length * 3/4可以被优化为(table.length >> 2) << 2) - (table.length >> 2) == table.length - (table.lenght >> 2),位运算的效率更高,3/4还兼顾了效率(前辈的代码写的真的太好了!)。
为什么选择链表长度大于8才转化为红黑树?
HashMap源码中有一段注释
* 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
0代表着,当桶中有0个数据的概率为:0.60653066;
……
8代表着,当桶中有8个数据的概率为:0.00000006。
也就是说,因hash冲突存放在同一个桶中的数据量为8的概率为0.00000006,这是一个很小的数字,当出现这种情况的时候,该桶中的长链表才会转化为红黑树;这样做是为了降低树出现在hashmap中的概率,毕竟长度为7的长链表也算不上很长。
当桶中存放的长链表已经变为树的时候,该树存放的数据并不是小于8的时候又会降为链表,而是在树中数据量小于6的时候降为链表;这样是为了避免在增减一个数据的时候节点数据量在8上下反复横跳造成频繁的链表和树的相互转化,所以将这个阈值错开了。
如何计算存放位置?
用key和(容量-1)做 & 运算
此时HashMap的容量为2的次幂的作用就会完美体现出来,例如容量为16,二进制为10000,减1就是01111;
可以看出,此时进入的4个不同的key值的数据,分别存入了四个不同的位置,有效降低了hash冲突,若是当容量不为2的次幂的时候呢?情况会是怎样的?
假如,HashMap的容量现在为10,10的二进制为01010,减1就是01001
可以看出,当容量为10的HashMap在存入四个不同key值的数据的时候,有多个计算出了在同一个位置(hash冲突),这样会使得三个数据存放在同一个数组节点中,会使得hash冲突严重,降低效率。
1.7中HashMap扩容死循环
HashMap是一个线程不安全的容器,在最坏的情况下,所有元素全部定位到同一个位置,形成一个长链表。
死循环只会出现在多线程情况下
JDK1.8以前采用头插法插入链表,即每次都在链表头部插入添加的数据;以下是1.7resize过程中的transfer(),此方法用来将扩容时旧链表中的元素倒插到新的链表中,也是导致HashMap扩容死循环的代码。
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
此时有一个HashMap如下图所示
扩容前:
若有两个线程需要插入新节点,恰好达到了loadFactor,两个线程中的HashMap都需要做resize;现在,线程1拿到了时间片进行resize,线程2在等待线程 1完成resize后再resize。
这是线程1resize后的:
注意,头插法使得a和b节点的顺序反转;这个时候的HashMap中的 a.next = null,b.next = a,newTable[i] = b。
在线程2的视角内,HashMap还是最开始没有扩容的状态,但是实际上已经变为上图的状态了;
线程2开始执行resize,获取a节点next = null 之后执行 a.next = new Table[i],即变为 a.next = b 这样就出现a-b-a的环形链表 c节点因为a.next = null 的原因,直接丢失了 此时还不会出现内存占用百分百的情况,当我们进行查询的时候,就会进入循环。
1.8中是怎样解决扩容死循环的
使用尾插法,之前使用头插法,会使得链表倒插,现在只需要将新数据存放到链表尾部
1.8的扩容优化
1.7时,需要重新计算每一个hash值
1.8时,通过(e.hash & oldCap) == 0来判断是否需要移位; 如果为真则在原位不动, 否则则需要移动到当前hash槽位 + oldCap(加旧容量)的位置,省去了rehash的步骤
old:
26: 0001 1010
16: 0001 0000 (容量)
&: 0001 0000
new:
26: 0001 1010
32: 0010 0000 (扩容后的容量)
&: 0000 0000
两次的差别,只在于左边第一位从1变为0,这个变化的1则是oldCap(例如上述例子,变化的1就是十进制的16,旧容量);若第一位从0变为1,则当前位置加上容量,就是resize后该数据应存放的位置。
这样的设计既省去了重新计算hash值的时间,又因为最左位是0还是1可以认为是随机的,这样resize的过程中,还会吧之前冲突的节点分散开。
1.8中HashMap就不会发生死循环了么?
不多说,直接上代码
public class TestHashMap extends Thread{
private static Map<Integer, Integer> map = new HashMap<Integer, Integer>(0);
private static AtomicInteger at = new AtomicInteger(0);
@Override
public void run() {
while(at.get()<1000000){
map.put(at.get(), at.get());
at.incrementAndGet();
}
}
public static void main(String[] args) {
for(int i=0;i<10;i++){
Thread thread = new TestHashMap();
thread.start();
}
}
}
如果没死循环,就多试几次
当程序不结束的时候,打开任务管理器,找到占用最高的java程序,找到PID号码,之后打开cmd,输入 jstack PID ,这样就可以得到报错信息
我试出了四个不同的报错信息(可能不止),将报错信息列出来
/** * java.lang.Thread.State: RUNNABLE * at java.util.HashMap$TreeNode.root(HashMap.java:1824) * at java.util.HashMap$TreeNode.putTreeVal(HashMap.java:1978) * at java.util.HashMap.putVal(HashMap.java:638) * at java.util.HashMap.put(HashMap.java:612) * at com.luck.ejob.server.MainTest$1.run(MainTest.java:151) * at java.lang.Thread.run(Thread.java:748) * * * * java.lang.Thread.State: RUNNABLE * at java.util.HashMap$TreeNode.balanceInsertion(HashMap.java:2239) * at java.util.HashMap$TreeNode.treeify(HashMap.java:1945) * at java.util.HashMap$TreeNode.split(HashMap.java:2180) * at java.util.HashMap.resize(HashMap.java:714) * at java.util.HashMap.putVal(HashMap.java:663) * at java.util.HashMap.put(HashMap.java:612) * at com.luck.ejob.server.MainTest$1.run(MainTest.java:151) * at java.lang.Thread.run(Thread.java:748) * * * * java.lang.Thread.State: RUNNABLE * at java.util.HashMap$TreeNode.treeify(HashMap.java:1948) * at java.util.HashMap$TreeNode.split(HashMap.java:2180) * at java.util.HashMap.resize(HashMap.java:714) * at java.util.HashMap.putVal(HashMap.java:663) * at java.util.HashMap.put(HashMap.java:612) * at main.TestHashMap.run(TestHashMap.java:22) * * * * java.lang.Thread.State: RUNNABLE * at java.util.HashMap$TreeNode.split(HashMap.java:2144) * at java.util.HashMap.resize(HashMap.java:714) * at java.util.HashMap.putVal(HashMap.java:663) * at java.util.HashMap.put(HashMap.java:612) * at main.TestHashMap.run(TestHashMap.java:22) * */
大家有兴趣的可以去看一下,我粗略看了一下,大概是红黑树出现环了。
多线程情况下,一定要使用 ConcurrentHashMap!!!