1、JDK1.7的HashMap:

       1.1、数据结构哈希表 + 链表

                                       并发编程(13):HashMap的线程安全问题分析以及ConcurrentHashMap的实现原理

          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的数据如下:

                                          并发编程(13):HashMap的线程安全问题分析以及ConcurrentHashMap的实现原理

                              2、线程A和B各自新增了一个新的哈希table,在线程A已做完扩容操作后,线程B才开始扩容。

                                          并发编程(13):HashMap的线程安全问题分析以及ConcurrentHashMap的实现原理

                                          当线程B完成将元素a做完头插法后:

                                          并发编程(13):HashMap的线程安全问题分析以及ConcurrentHashMap的实现原理

                                           线程B开始处理元素b了,由于这个时候线程A已经将b的next改为了a,头插法插入b情况如下:

                                               并发编程(13):HashMap的线程安全问题分析以及ConcurrentHashMap的实现原理

                                            由于此时对于线程B来说,next是又是a元素了,a元素的next是null,那么线程B又会再次头插法a:

                                              并发编程(13):HashMap的线程安全问题分析以及ConcurrentHashMap的实现原理

                                                这就形成了环链,而且造成了数据c的丢失。

 

2、JDK1.8的HashMap:

          2.1、数据结构哈希表 + 链表(或红黑树):

                      并发编程(13):HashMap的线程安全问题分析以及ConcurrentHashMap的实现原理

          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中是采用尾插法,因此不会出现环链的问题。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

相关文章: