1、JDK1.7的HashMap:
1.1、数据结构:哈希表 + 链表
1.2、解决hash冲突的方式就是链址法,就是通过key计算的index重复的话,就使用头插法加入链表。
1.3、特殊key值处理,什么是特使key,也就是key是null,其结论如下:
Ⅰ HashMap中,是允许key、value都为null的,且key为null只存一份,多次存储会将旧value值覆盖;
Ⅱ key为null的存储位置,都统一放在下标为0的bucket,即:table[0]位置的链表;
Ⅲ 如果是第一次对key=null做put操作,将会在table[0]的位置新增一个Entry结点,使用头插法做链表插入。
1.4、扩容,当哈希表中存放的k-v对数量超过了当前阈值(threshold = table.length * loadFactor),且当前的bucket下标有链表存在,那么就做扩容处理(resize)。扩容后,重新计算hash,最终得到新的bucket下标,然后使用头插法新增结点。
扩容的两大核心就是:
Ⅰ扩容后大小是扩容前的2倍,一般指的是哈希表的长度;
Ⅱ扩容后,重新计算hash,最终得到新的bucket下标,然后使用头插法新增结点。
1.5、线程安全问题;
Ⅰ多线程同时做写入操作会导致数据覆盖:当线程A和线程B都获取到了bucket的头结点后,若此时线程A的时间片用完,线程B将其新数据完成了头插法操作,此时轮到线程A操作,但这时线程A所据有的旧头结点已经过时了(并未包含线程B刚插入的新结点),线程A再做头插法操作,就会抹掉B刚刚新增的结点,导致数据丢失。
Ⅱ扩容的时候导致死循环:演示如下;
1、假设当前的HashMap的数据如下:
2、线程A和B各自新增了一个新的哈希table,在线程A已做完扩容操作后,线程B才开始扩容。
当线程B完成将元素a做完头插法后:
线程B开始处理元素b了,由于这个时候线程A已经将b的next改为了a,头插法插入b情况如下:
由于此时对于线程B来说,next是又是a元素了,a元素的next是null,那么线程B又会再次头插法a:
这就形成了环链,而且造成了数据c的丢失。
2、JDK1.8的HashMap:
2.1、数据结构:哈希表 + 链表(或红黑树):
2.2、 解决hash冲突的方式就是链址法,就是通过key计算的index重复的话,就使用尾插法加入链表,在JDK7中,新增结点是使用头插法,但在JDK8中,在链表使用尾插法,将待新增结点追加到链表末尾。
2.3、当链表的长度达到默认的长度8后就会转为红黑树来提供查询效率。
2.4、扩容,当哈希表中存放的k-v对数量超过了当前阈值(threshold = table.length * loadFactor),且当前的bucket下标有链表存在,那么就做扩容处理(resize)。扩容后,重新计算hash,最终得到新的bucket下标,然后使用尾插法新增结点。
2.5、线程安全问题:
Ⅰ多线程同时做写入操作会导致数据覆盖:当线程A和线程B都获取到了bucket的头结点后,若此时线程A的时间片用完,线程B将其新数据完成了头插法操作,此时轮到线程A操作,但这时线程A所据有的旧头结点已经过时了(并未包含线程B刚插入的新结点),线程A再做头插法操作,就会抹掉B刚刚新增的结点,导致数据丢失。
Ⅱ由于JDK1.8中是采用尾插法,因此不会出现环链的问题。
3、ConcurrentHasnMap实现原理:
观看了蛮多ConcurrentHasnMap实现原理的博客,这一篇讲得挺好,在此借用一下,如有版权问题请联系我修改。
https://blog.csdn.net/ddxd0406/article/details/81389583