HashMap 简介
- HashMap 主要用来存放键值对,它基于哈希表的 Map 接口实现,是常用的 Java 集合之一,是线程不安全的。 HashMap;
- 可以存储 null 的 key 和 value ,但 null 作为 key 只能有一个,null 作为值可以有多个;
- JDk 1.8 之前 HashMap 底层是由数组+链表实现的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(拉链法解决冲突)。JDK 1.8 及之后的 HashMap 在解决哈希冲突时使用了红黑树,当链表长度大于等于阈值(默认为8)并且数组长度超过64时(未超过64会先将数组扩容),将链表转化为红黑树,以减少所搜时间;
- HashMap 默认的初始大小为16,负载因子为0.75,数组使用超过 16 * 0.75 =12 后就将进行扩容,每次扩充容量变为原来的2倍。并且,HashMap 总是使用2的幂作为哈希表的大小;
为什么使用红黑树
- 在使用红黑树之前,即使哈希函数取值再好,也很难达到元素完全均匀分布,当 HashMap 中有大量的元素都存放在一个数组节点时,这个节点下就有一条长链表,这时候 HashMap 就相当于一个单链表,查找的时间复杂度为O(n),失去了其优势;
- 针对这种情况,JDK 1.8 中引入了红黑树(红黑树的查找时间复杂度为:O(logn) )来优化这个问题,当链表长度很小的时候,即使遍历,速度也很快;
为什么单链表长度超过8时才转为红黑树结构
- 因为树节点所占空间是普通节点的两倍,所以只有当节点足够多的时候,才会使用树节点。
- 源码上说,为了配合使用分布良好的 hashCode ,树节点很少使用;
- 在理想情况下,受随机分布的 hashCode 影响,链表中的节点遵守泊松分布,而且根据统计,链表节点数是8的概率已经非常小了,只有在这种比较罕见和极端的情况下,才会把链表转变为红黑树;
- 链表转化为红黑树也是需要消耗性能的;
为什么 HashMap 线程不安全
- 非同步操作:HashMap 的实现不是线程安全的,它没有内部机制来处理多个线程同时访问或修改 HashMap 的情况。如果多个线程对 HashMap 进行插入、删除或更新操作,可能会导致数据的不一致或损坏;
- 容量扩容:HashMap 在扩容时,需要重新计算元素的哈希值并重新分配存储位置,这个过程涉及到对原数组进行复制和重新插入元素的操作。如果在扩容期间有其他线程对 HashMap 进行并发修改,就可能导致数据丢失或出现异常;
- 非原子操作:HashMap 的操作不是原子性的,例如 put() 方法涉及到了多个步骤,包括计算哈希值、查找和插入元素等。如果多个线程同时执行这些操作,就有可能导致数据不一致的情况
解决方法:
- 使用同步机制:可以使用线程安全的 Map 实现,如 ConcurrentHashMap ,或者通过在访问 HashMap 时使用 synchronized 或其他锁机制来确保同一时间只有一个线程能够修改 HashMap;
- 使用线程封闭:可以将 HashMap 封闭在单个线程中,通过使用 ThreadLocal 或将 HashMap 作为局部变量在每个线程中进行操作,从而避免多线程访问导致线程安全问题;