HashMap是一种存取高效但是不保证有序的常用容器。它的数据结构为“数组+链表”,这是解决哈希冲突的产物(链地址法)。它实现了Map接口,采用KEY-VALUE键值对的形式存储数据,并且实现了浅拷贝和序列化。
HashMap 的默认初始大小为16,初始化大小必须为2的幂,最大大小为2的30次方。数组中存储的链表节点为Entry数组,在jdk1.8源码中将Entry改为了Node,它们都实现了Map下的Entry接口。HashMap 的阈值默认为“容量*0.75f”,当存储节点数量超过该值,则对map 进行扩容处理。
HashMap 提供了4种构造方法,分别是默认构造方法;可以指定初始容量的构造方法;可以指定初始容量和阈值的构造方法以及基于一个Map 的构造方法。虽然是构造函数,但是真正的初始化都是在第一次添加操作里面实现的。
在第一次添加操作中,HashMap 会先判断存储数组有没有初始化,如果没有先进行初始化操作,初始化过程中会取大于等于用户指定容量的最小2 的幂次方数作为数组的初始容量,并更新扩容的阈值。
HashMap的put流程:
1、先判断有没有初始化
2、再判断传入的key 是否为空,为空保存在table[0] 位置
3、key 不为空就对key 进hash,hash 的结果再和数组的长度进行&与操作就得到存储的位置index
4、如果存储位置为空则创建节点,不为空就说明存在冲突
5、解决冲突HashMap 会先遍历链表,如果有相同的value 就更新旧值,否则构建节点添加到链表头
6、添加还要先判断存储的节点数量是否达到阈值,到达阈值要进行扩容
7、扩容扩2倍,是新建数组所以要先转移节点,转移时都重新计算存储位置,可能保持不变可能为旧容量+位置。
8、扩容结束后新插入的元素也得再hash 一遍才能插入。
HashMap的get流程
1、先判断是否为空,为空就在table[0] 去找值
2、不为空也是先hash,&数组长度计算下标位置
3、再遍历找相同的key 返回值
在并发情况下:
HashMap 是一个并发不安全的容器,在迭代操作是采用的是fast-fail 机制;在并发添加操作中会出现丢失更新的问题;因为采用头插法在并发扩容时会产生环形链表的问题,导致CPU 到达100%,甚至宕机。
fast-fail: 我们知道 java.util.HashMap 不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出 ConcurrentModificationgException,这就是所谓 fail-fast 策略。
解决方案:
1、Java 类库提供的Collections 工具包下的Collections.synchronizedMap()方法,返回一个线程安全的Map
2、使用并发包下的 ConcurrentHashMap,ConcurrentHashMap采用分段锁机制实现线程安全
3、使用HashTable (不推荐)
JDK1.7和JDK1.8 HashMap的区别:
1、在hash 取下标时将1.7 的9次扰动(5次按位与和4次位运算)改为2次(一次按位与和一次位运算)
2、1.7 的底层节点为Entry,1.8 为node ,但是本质一样,都是Map.Entry 的实现
3、还有就是在存取数据时添加了关于树结构的遍历更新与添加操作,并采用了尾插法来避免环形链表的产生
4、但是并发丢失更新的问题依然存在。