Java面试准备——HashMap底层原理以及ConcurrentHashMap

HashMap底层原理

本文学习自GitHub上的JavaGuide项目,感谢大佬的资源,此处为自我学习与整理,原项目链接 JavaGuide

无论是平时做LeetCode还是准备面试,HashMap都是重点中的重点。所以可以说了解HashMap的底层原理是一个后端程序员的基本入门条件。

JDK1.8以前

在JDK1.8之前,HashMap底层采用数组+链表的数据结构(链表散列)。HashMap通过Key的hashcode经过扰动函数处理得到hash值,然后通过(n-1)&hash判断当前元素存放的位置,(此处n是数组的长度),如果这个位置已经存在元素,判断新加入的元素与其hash值以及Key是否相同,相同直接覆盖原有元素,不相同则使用拉链法解决冲突(从这个位置创建链表)。

扰动函数:扰动函数就是HashMap的hash方法,使用hash方法的目的是为了防止一些实现比较差的HashCode()方法,其实就是为了减少哈希碰撞。

JDK1.8的hash function:

static final int hash(Object key) {
        int h;
        // key.hashCode():返回散列值也就是hashcode
        // ^ :按位异或
        // >>>:无符号右移,忽略符号位,空位都以0补⻬
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

拉链法:当遇到hash冲突时,在数组的位置创建链表,将hash冲突的元素存入链表。

JDK1.8之后

JDK1.8之后,对于HashMap的拉链法进行了改进。当链表长度大于阔值(默认为8,由泊松分布统计而得)时使用红黑树的数据结构来存储,红黑树的优点是搜索的时间复杂度低。

为什么不适用AVL树、BST?

AVL树的层数差最多为1,所以具有更快的查询速度,但是牺牲了插入的速度。相比于AVL树红黑树不是完全的平衡,使得红黑树插入的效率高于AVL树,取舍之下红黑树更胜一筹。
BST在极端情况下会线性化,变成链表。

HashMap的长度为什么是2的n次幂?

hash值的取值范围是int的取值范围,也就是有大概40亿的映射空间,所以只要把hash映射的足够均匀一般是不会有冲突的。但是这么长的数组内存是根本不够用的,所以这个散列值是不能拿来直接用的。在用之前对这个数组的长度进行取模运算,用得来的余数作为对应数组下标。这个数组下标计算方式是(n - 1) & hash。

这个算法的设计思路:
取余操作如果除数是2的幂次方则等价于其除数-1的与操作(&),也就是说hash%length==(n - 1) & hash的前提是length是2的幂次方,并且采用二进制运算&比%运算效率高,所以length是2的幂次方,这就解释了HashMap的长度为什么是2的n次幂。

HashMap在多线程中遇到的问题

死循环:并发情况下HashMap的Rehash过程可能造成元素之间形成环状链表。1.8之后解决了这个问题,不过还是不建议在多线程中使用HashMap,因为可能存在数据丢失。并发环境下有专门用的ConcurrentHashMap。

ConcurrentHashMap

两者都是用于并发,有什么区别呢?ConcurrentHashMap又是凭借什么优势淘汰了HashTable呢?

两者的区别主要体现在线程安全的实现方式

  1. 底层数据结构:1.7时ConcurrentHashMap底层使用分段数组+链表实现,1.8和HashMap底层结构相同。
  2. 实现线程安全的方法:
    1.7时ConcurrentHashMap(分段锁)对整个桶数组进行了分割分段(segment),每一把锁只锁容器中一部分的数据。多线程访问不同的数据区域,就不会产生冲突,也不会产生锁竞争,提高了并发访问率。
    1.8以后摒弃了segment,直接使用Node数组+链表+红黑树的底层数据结构,并发控制则使用synchronized和CAS来操作。看起来就像优化的线程安全的HashMap。
    HashTable则是使用同一把锁,synchronized关键字保证线程安全,但是效率十分低下。当一个线程访问同步方法时其他线程进入阻塞状态。

图源JavaGuide http://www.cnblogs.com/chengxiao/p/6842045.html , HashTable与1.7,1.8版本的ConcurrentHashMap对比:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

ConcurrentHashMap线程安全的底层实现原理

  1. JDK1.7中采用分段锁(segment),每个线程只能访问不同的段,提高了并发率并且线程安全。

    ConcurrentHashMap 是由Segment数组结构和HashEntry数组结构组成的
    Segment实现了ReentrantLock,所以Segment是一种可重入锁,扮演锁的角色,HashEntry用来储存键值对。

static class Segment<K,V> extends ReentrantLock implements Serializable {
}
  1. 一个ConcurrentHashMap中包含一个Segment数组,其结构和HashMap类似,是一种数组+链表的结构,一个Segment包含一个HashEntry数组,每个HashEntry都是一个链表结构的元素,而每个Segment保证了其中HashEntry的线程安全:想要修改HashEntry必须获得对应的Segment的锁。
  2. JDK1.8中的ConcurrentHashMap取消了Segment分段锁。直接使用synchronized和CAS来保证并发安全。数据结构和HashMap类似:数组+链表+红黑树。synchronized只锁当前链表或者红黑树的首节点,这样只要hash不冲突,就不会产生并发问题,极大的提升了效率。
发布了5 篇原创文章 · 获赞 0 · 访问量 1911

猜你喜欢

转载自blog.csdn.net/weixin_40407203/article/details/105213675