HashMap的常规知识点总结
为什么HashMap采用数组加链表的数据结构
采用数组是因为数组检索的时间复杂度是O(1),检索效率高。引入链表是为了解决hash冲突。
hash冲突的解决方案有哪些
rehash
引入链表
建立公共溢出区域
开放定址法:线性探测再散列、二次(平方)探测再散列、伪随机探测再散列
HashMap的扩容机制
数组容量大于临界值,同时插入的槽位上存在数据时,数组才会发生扩容。扩容后数组的大小是原先大小的2倍,最大为2的30次方。
HashMap不安全的原因
多线程put并发的时候可能造成数据的丢失
我们假设三个线程同时运行,每个线程往HashMap里面插入一个KV键值对,分别为KV1、KV2、KV3。假设KV1、KV2、KV3中key的equals()方法判断相等且hash值一致,即它们都插入到数组的同一个槽位中。线程1的KV1正常插入,然后线程2的KV2执行完上图的位置1时,失去了cpu的执行权限,发生了阻塞,然后线程3的KV3在线程2阻塞时,也执行完了位置1处。此时,线程2和线程3中,变量e指向的都是线程1中插入的KV1。线程2和线程3继续往下执行,如果是线程2先执行完成,线程3后执行完成,那么线程3的KV3将会覆盖掉线程2的KV2;如果是线程3先执行完成,线程2后执行完成,那么线程2的KV2将会覆盖掉线程3的KV3。此时就会导致数据的丢失!!!
多线程put和get并发的时候,可能导致get为null或取到的元素不对
put放入元素时,如果达到临界值且插入的槽位已有元素,就会发生扩容,数组的扩容就伴随着rehash,rehash就会导致Entry的槽位可能发生变化。当多线程并发时,线程1调用put方法插入元素,线程2调用get方法获取元素。线程2计算完key的hash值,确定了待获取的元素的在数组的索引,但是还没有获取到数据。此时,线程1插入元素时,触发了数组扩容,所有的元素都进行了rehash,原先线程2想要获取的元素经过rehash后,放到了数组的另一个槽位中,那么,线程2在数组扩容后,再去原先的数组索引处取元素,就可能会取到null值或者是另外一个非预期的元素。
并发扩容
问题1:JDK1.7数组扩容时,元素的移动是采用的头插法,可能会产生循环引用,导致内存泄漏,cpu使用率飙升。
假设数组同一个槽位上有a、b、c三个元素且a指向b,b指向c,形成链表结构,假设abc三个元素扩容后还是存储在同一个槽位。多线程并发时,线程1执行到Entry<K,V> next = e.next;完成赋值后,短暂阻塞,此时线程1中e为a元素,e.next和局部变量next为b元素。线程2正常执行完扩容操作,此时table里面元素为c指向b,b指向a。线程2完成扩容后,因为数组中的元素是线程共享的,所以线程1此时元素的状态也是c指向b,b指向a。线程1继续执行,根据线程1阻塞前的赋值情况,第一次while循环为a元素,a元素的next节点赋值为null,然后把数组槽位上的值设置为a元素;根据线程1阻塞前的赋值情况,第二次循环为b元素,此时b元素的next指向的元素为a,所以下一次循环为a元素,然后把b的next节点设置为数组槽位上的值,即b的next节点指向a元素,然后把数组槽位上的值设置为b元素;第三次循环为a元素,a元素的next节点为null,所以执行结束会跳出while循环,然后把a的next节点设置为数组槽位上的值,即a的next节点指向b元素,然后把数组槽位上的值设置为a元素。
循环结束,此时a元素指向b元素,b元素指向a元素,循环引用。当查询HashMap中的元素时,如果查询的元素既不是a也不是b,一旦遍历到a元素和b元素所在的槽位,就会形成死循环。
问题2:线程1和线程2在插入新元素时,都判断需要进行扩容,但是线程2判断完需要扩容后,进行了短暂的阻塞,等线程1扩容完成后,线程2继续进行。此时,线程2获取到的原数组就是线程1扩容后的数组,这样就会导致数组的容量再次扩大。但是这种情况并不是错误,只是会导致扩容后的数组比预期的大,浪费了存储空间而已。