备战秋招—HashMap与ConcurrentHashMap的前世今生

备战秋招—HashMap与ConcurrentHashMap的前世今生

备战秋招,Java集合的重要程度不言而喻,今天就来聊聊Java集合中的重中之重HashMap,直接进入模拟面试场景。
面试官:小伙子来聊聊java集合吧?
瑟瑟发抖的我:集合框架的父接口有Map接口Collection接口,Map接口的实现类主要有:HashMap、TreeMap、ConcurrentHashMap、Hashtable等。Collection接口的子接口包括:Set接口和List接口,Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等,List接口的实现类主要有:ArrayList、LinkedList、Vector等。
面试官:嗯,好的,刚才听你说聊到了HashMap,和我聊聊HashMap吧,把你知道的都说出来。
继续瑟瑟发抖的我:HashMap是Map的一个实现类,Map存储形式是键值对(key,value)的。HashMap在JDK1.7版本,存储结构是数组 + 链表,而在JDK1.8,变为了数组 + 链表 + 红黑树。在JDK1.7,hash值计算方式为扰动处理 多次位运算 + 多次异或运算,而在1.8中仅仅需要1次位运算 + 1次异或运算。并且插入方式也存在不同,JDK1.7,采用头插法,将原来位置的数据移到后1位,再插入数据到该位置,1.8变为了尾插法,插入到链表尾部/红黑树。当链表中的数据较多时,查询的效率会下降,所以在JDK1.8版本后做了一个升级,hashmap就是当链表中的元素达到8并且元素数量大于64时,会将链表替换成红黑树才会树化时,会将链表替换成红黑树,来提高查找效率。因为对于搜索,插入,删除操作多的情况下,使用红黑树的效率要高一些。
面试官:打断一下,你有了解哈希冲突么?能解释一下你说的扰动处理嘛,HashMap是如何来解决哈希冲突的?
哆哆嗦嗦的我:有些了解的,哈希冲突就是,两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,就把它称为哈希碰撞。数组:寻址容易,插入和删除困难;链表:寻址困难,但插入和删除容易;我们将数组和链表结合在一起,使用一种叫做链地址法的方式可以解决哈希冲突。HashMap初始的容量大小为16要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表,所以我们还需要对hashCode作一定的优化,如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,把hashCode右移16位,与自己进行异或运算,这样降低了hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动。我记得源码是这样优化的

static final int hash(Object key) {
     
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 
}

总结一下HashMap是如何来解决哈希冲突的

  1. 使用散列表来链接拥有相同hash值的数据;
  2. 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
  3. 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快;

面试官:小伙子,不错啊,那你知道为什么数组长度要保证为2的幂次方嘛?
听了夸奖不太抖的我:当数组长度为2的幂次方时,h&(length-1)才等价于h%length,即实现了key的定位,2的幂次方也可以减少冲突次数,提高HashMap的查询效率;
length 为 2 的次幂 ,所以 length-1 转化为二进制是 11111……的形式,在于 h 的二进制与操作效率会非常的快,空间充分利用;如果 length 不是 2 的次幂,空间浪费大,数组可以使用的位置比数组长度小了很多,进一步增加了碰撞的几率,减慢了查询的效率!
面试官:1.8中为什么需要两次扰动?能解释一下这样做的好处么?三次不行么?
不会问题又抖了起来的我:这,这个我不太清楚,没有深入去了解。
面试官:两次扰动,加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的,没关系的,这你下去再了解一下。
HashMap是线程安全的么?平时的话对于多线程场景下是如何保证线程安全的?
瑟瑟发抖的我:HashMap是线程不安全的,在多线程环境下,使用Hashmap进行put操作会引起死循环,导致CPU利用率接近100%,而且会抛出并发修改异常,导致原因是并发争取线程资源,修改数据导致的,一个线程正在写,一个线程过来争抢,导致线程写的过程被其他线程打断,导致数据不一致。平常我都用ConcurrentHashMap。
面试官:为什么不用HashTable?
心情慢慢平缓下来的我:HashTable是线程安全的实现代价却太大了,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁。多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。
面试官:那你简单说说ConcurrentHashMap。
渐渐充满自信的我:在JDK1.7版本中ConcurrentHashMap避免了对全局加锁,改成了局部加锁(Segment),segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。不过这种结构的带来的副作用是Hash的过程要比普通的HashMap要长。
所以在JDK1.8版本中,采用Node数组 + CAS + Synchronized来保证并发安全进行实现,CurrentHashMap内部中的value使用volatile修饰,保证并发的可见性以及禁止指令重排,只不过volatile不保证原子性,使用为了确保原子性,采用CAS(比较交换)这种乐观锁来解决。
面试官:今天时间不早了,今天的面试就到这里吧,后面会有人联系你的,等待消息吧。

你可能感兴趣的:(备战秋招,java,hashmap)