常见集合系列之JDK1.8中HashMap

上期我们提到jdk1.7中的hashMap,在校招或者初级Java开发的面试中经常会问到,尤其是jdk1.8版本的!其实与1.7相比,1.8还是在很多方面做了优化,像引入红黑树、头插变尾插、扰动函数调整等,即使做了这么多调整,可惜在多线程的环境下依旧不能使用,接下来回顾一下上节的内容吧!

image.png 聪明的你,相信看完上面这张图就可以回顾起上节咱们聊过的问题吧!但是我还是建议在座的各位好好看看底层的源码比较好些,然后在多线程的环境下测试一下,用jps和jstack命令查看一下堆栈信息。好了,说了这么多,咱们开始进入1.8的内容吧!

一、数据结构变化

还记得1.7中hashMap的结构是Entry数组+链表吧!1.8中变成了Node数组+链表+红黑树的结构,下面浅看一下源码

carbon.png 一定要注意一下什么情况下会转成红黑树,这里抛出两个问题:当链的长度等于8时一定会转成红黑树吗? 以及它在什么时候进行扩容操作的?只在map的大小等于阈值的时候才会触发扩容操作吗?确定吗? 以上两个问题,下文我会进行解答的哦!要是我忘了解答,记得在评论区call我哦!

我觉得还是画图来的实际点!这样的话也看的更清楚点嘛!

image.png

二、死循环

HashMap<String, String> map = new HashMap<String, String>();

class MyThread implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 200000; i++) {
            // 生成随机key
            String key = UUID.randomUUID().toString().substring(0, 5);
            String value = UUID.randomUUID().toString().substring(0, 5);
            // 执行put操作
            map.put(key, value);
        }
        System.out.println("====当前线程名:" + Thread.currentThread().getName() + "插入完成====");
    }
}


public static void main(String[] args) {
    Test test = new Test();
    // 10个线程
    int i=0;
    while (i<3) {
        Thread thread = new Thread(test.new MyThread());
        thread.start();
        i++;
    }
}
复制代码

执行后,发现控制台卡死,我们用jps和jstack命令查看堆栈报错信息,发现是在调用balanceInsertion时,出现的问题 image.png debug后我们发现节点x、xp、xpp、xppl、xppr都是一样的,会进入if ((xppr = xpp.right) != null && xppr.red) 中,然后继续将xpp赋值给x,陷入无穷无尽的死循环

image.png

三、数据覆盖

jdk1.7版本的hashMap因采用了头插导致数据丢失嘛!在jdk1.8版本的情况下,采用尾插的方式取代了头插,难道就不会有数据丢失的问题吗?

package com.cheers;

import java.util.*;
import java.util.concurrent.CountDownLatch;

/**
 * @ClassName Test
 * @Description TODO
 * @Author Cheers
 * @Date 2022/5/15 11:23
 * @Version 1.0
 */
public class Test {

    static List<String> list = Collections.synchronizedList(new LinkedList<String>());
    static HashMap<String, String> map = new HashMap<String, String>();
    static CountDownLatch latch = new CountDownLatch(2);

    class MyThread implements Runnable {

        @Override
        public void run() {
            try {
                for (int i = 0; i < 200000; i++) {
                    // 生成随机key
                    String key = UUID.randomUUID().toString().substring(0, 5);
                    String value = UUID.randomUUID().toString().substring(0, 5);
                    // 执行put操作
                    list.add(key);
                    map.put(key, value);
                }
                System.out.println("====当前线程名:" + Thread.currentThread().getName() + "插入完成====");
            } finally {
                latch.countDown();
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {
        Test test = new Test();
        // 2个线程
        int i = 0;
        while (i < 2) {
            Thread thread = new Thread(test.new MyThread());
            thread.start();
            i++;
        }
        // 数据丢失
        latch.await();
        int loseNum = 0;
        for (String key : list) {
            if (null == map.get(key) || map.get(key).isEmpty()) {
                loseNum++;
            }
        }
        System.out.println("=============");
        System.out.println("list大小:" + list.size() + "丢失数量:" + loseNum);
    }
}
复制代码

最后打印出来的结果发现还是存在数据丢失的情况,确实有点奇怪!既然不是尾插导致的数据丢失,那还有什么情况会存在这个问题呢?

image.png 查看了很多博客,里面都提到在多线程的环境下会发生数据覆盖的情况,假设有线程A、B两个线程,这两个线程计算出的hash值都是一样的,根据(n - 1)& hash得出在table中对应的位置i,此时线程A恰好挂起,还未执行插入操作,线程B完成正常的插入操作后,线程A恢复执行,恰好将线程B的值给覆盖掉了 carbon (1).png

四、扩容

面试官经常会问到hashMap的扩容原理,是在什么时候发生扩容的?熟读面试八股文的小伙伴一般都会回答当hashmap的大小超过阈值的时候,就会调用resize进行扩容操作!哈哈,我以前就是这么回答的,想想也真可爱、年轻啊!但是看一下resize的源码我们就会发现,没那么简单!!!newCap变为原来的2倍的前提是oldCap大于0且小于1<<30,记住:在第一次进行put操作的时候,newCap会赋为默认值16 carbon.png 我们知道jdk1.8引入了红黑树,树化的条件是table的长度超过最小树化容量64,且链的长度大于等于8的时候采用转成红黑树,在treeifyBin方法中也会调用resize,进行数组扩容操作 carbon (1).png

五、负载因子为何为0.75?

查看oracle官网关于负载因子为啥是0.75,其中提到0.75提供了时间和空间之间良好的平衡
As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur.(一般来说,默认的负载因子(.75)在时间和空间成本之间提供了一个很好的权衡。较高的值会减少空间开销,但会增加查找成本(反映在HashMap类的大多数操作中,包括get和put)。在设置map的初始容量时,应该考虑map中预期的条目数量及其负载因子,以尽量减少rehash操作的数量。如果初始容量大于最大条目数除以负载因子,则不会发生重hash操作)

六、总结

好啦!本期关于jdk1.8的hashmap的介绍就到这里了!回顾一下,本期介绍了hashMap数据结构的调整、多线程环境下的死循环以及数据覆盖问题,hashMap的扩容、负载因子的选择问题!当然还有so much很多需要补充的,像1.8进行resize时为啥取消rehash、为啥链表长度为8时转红黑树,6时切回链表等等...
在此抛出一个问题,为啥链表要转红黑树?直接用红黑树不好吗?

参考: blog.csdn.net/jarniyy/art… docs.oracle.com/javase/8/do…

猜你喜欢

转载自juejin.im/post/7105399577548701732
今日推荐