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

    技术2022-07-11  101

    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

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    Processed: 0.013, SQL: 9