【ros2源码安装】【ltg源码分析】【fusionapp导入源码】1.8map扩容源码_扩容代码

时间:2024-12-24 03:56:13 编辑:导出教务处源码 来源:酷酷爱魔兽 源码

1.hashmap1.7和1.8的容源容代区别
2.concurrenthashmap1.8源码如何详细解析?
3.HashMap为什么不安全?
4.concurrenthashmap1.7和1.8的区别

1.8map扩容源码_扩容代码

hashmap1.7和1.8的区别

       HashMap是我们开发中经常使用到的集合,jdk1.8相对于1.7底层实现发生了一些改变。码扩码1.8主要优化减少了Hash冲突 ,容源容代提高哈希表的码扩码存、取效率。容源容代

       底层数据结构不一样,码扩码ros2源码安装1.7是容源容代数组+链表,1.8则是码扩码数组+链表+红黑树结构(当链表长度大于8,转为红黑树)。容源容代

       JDK1.8中resize()方法在表为空时,码扩码创建表;在表不为空时,容源容代扩容;而JDK1.7中resize()方法负责扩容,码扩码ltg源码分析inflateTable()负责创建表。容源容代

       1.8中没有区分键为null的码扩码情况,而1.7版本中对于键为null的容源容代情况调用putForNullKey()方法。但是两个版本中如果键为null,那么调用hash()方法得到的都将是0,所以键为null的元素都始终位于哈希表table0中。

       当1.8中的桶中元素处于链表的情况,遍历的同时最后如果没有匹配的,直接将节点添加到链表尾部;而1.7在遍历的同时没有添加数据,而是另外调用了addEntry()方法,将节点添加到链表头部。fusionapp导入源码

       1.7中新增节点采用头插法,1.8中新增节点采用尾插法。这也是为什么1.8不容易出现环型链表的原因。

       1.7中是通过更改hashSeed值修改节点的hash值从而达到rehash时的链表分散,而1.8中键的hash值不会改变,rehash时根据(hash&oldCap)==0将链表分散。

       1.8rehash时保证原链表的顺序,而1.7中rehash时有可能改变链表的顺序(头插法导致)。

       在扩容的时候:

       1.7在插入数据之前扩容,而1.8插入数据成功之后扩容。

concurrenthashmap1.8源码如何详细解析?飞起走兽源码

       ConcurrentHashMap在JDK1.8的线程安全机制基于CAS+synchronized实现,而非早期版本的分段锁。

       在JDK1.7版本中,ConcurrentHashMap采用分段锁机制,包含一个Segment数组,每个Segment继承自ReentrantLock,并包含HashEntry数组,每个HashEntry相当于链表节点,用于存储key、value。默认支持个线程并发,每个Segment独立,转转源码2019互不影响。

       对于put流程,与普通HashMap相似,首先定位至特定的Segment,然后使用ReentrantLock进行操作,后续过程与HashMap基本相同。

       get流程简单,通过hash值定位至segment,再遍历链表找到对应元素。需要注意的是,value是volatile的,因此get操作无需加锁。

       在JDK1.8版本中,线程安全的关键在于优化了put流程。首先计算hash值,遍历node数组。若位置为空,则通过CAS+自旋方式初始化。

       若数组位置为空,尝试使用CAS自旋写入数据;若hash值为MOVED,表示需执行扩容操作;若满足上述条件均不成立,则使用synchronized块写入数据,同时判断链表或转换为红黑树进行插入。链表操作与HashMap相同,链表长度超过8时转换为红黑树。

       get查询流程与HashMap基本一致,通过key计算位置,若table对应位置的key相同则返回结果;如为红黑树结构,则按照红黑树规则获取;否则遍历链表获取数据。

HashMap为什么不安全?

       åŽŸå› ï¼š

       JDK1.7 中,由于多线程对HashMap进行扩容,调用了HashMap#transfer(),具体原因:某个线程执行过程中,被挂起,其他线程已经完成数据迁移,等CPU资源释放后被挂起的线程重新执行之前的逻辑,数据已经被改变,造成死循环、数据丢失。

       JDK1.8 中,由于多线程对HashMap进行put操作,调用了HashMap#putVal(),具体原因:假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

       æ”¹å–„:

       æ•°æ®ä¸¢å¤±ã€æ­»å¾ªçŽ¯å·²ç»åœ¨åœ¨JDK1.8中已经得到了很好的解决,如果你去阅读1.8的源码会发现找不到HashMap#transfer(),因为JDK1.8直接在HashMap#resize()中完成了数据迁移。

       2、HashMap线程不安全的体现:

       JDK1.7 HashMap线程不安全体现在:死循环、数据丢失

       JDK1.8 HashMap线程不安全体现在:数据覆盖

       äºŒã€HashMap线程不安全、死循环、数据丢失、数据覆盖的原因

       1、JDK1.7 扩容引发的线程不安全

       HashMap的线程不安全主要是发生在扩容函数中,其中调用了JDK1.7 HshMap#transfer():

void transfer(Entry[] newTable, boolean rehash) {

          int newCapacity = newTable.length;

          for (Entry<K,V> e : table) {

              while(null != e) {

                  Entry<K,V> next = e.next;

                  if (rehash) {

                      e.hash = null == e.key ? 0 : hash(e.key);

                  }

                  int i = indexFor(e.hash, newCapacity);

                  e.next = newTable[i];

                  newTable[i] = e;

                  e = next;

              }

          }

       }

       å¤åˆ¶ä»£ç 

       è¿™æ®µä»£ç æ˜¯HashMap的扩容操作,重新定位每个桶的下标,并采用头插法将元素迁移到新数组中。头插法会将链表的顺序翻转,这也是形成死循环的关键点。理解了头插法后再继续往下看是如何造成死循环以及数据丢失的。

       2、扩容造成死循环和数据丢失

       å‡è®¾çŽ°åœ¨æœ‰ä¸¤ä¸ªçº¿ç¨‹A、B同时对下面这个HashMap进行扩容操作:

       æ­£å¸¸æ‰©å®¹åŽçš„结果是下面这样的:

       ä½†æ˜¯å½“线程A执行到上面transfer函数的第行代码时,CPU时间片耗尽,线程A被挂起。即如下图中位置所示:

       æ­¤æ—¶çº¿ç¨‹A中:e=3、next=7、e.next=null

       å½“线程A的时间片耗尽后,CPU开始执行线程B,并在线程B中成功的完成了数据迁移

       é‡ç‚¹æ¥äº†ï¼Œæ ¹æ®Java内存模式可知,线程B执行完数据迁移后,此时主内存中newTable和table都是最新的,也就是说:7.next=3、3.next=null。

       éšåŽçº¿ç¨‹A获得CPU时间片继续执行newTable[i] = e,将3放入新数组对应的位置,执行完此轮循环后线程A的情况如下:

       æŽ¥ç€ç»§ç»­æ‰§è¡Œä¸‹ä¸€è½®å¾ªçŽ¯ï¼Œæ­¤æ—¶e=7,从主内存中读取e.next时发现主内存中7.next=3,此时next=3,并将7采用头插法的方式放入新数组中,并继续执行完此轮循环,结果如下:

       æ­¤æ—¶æ²¡ä»»ä½•é—®é¢˜ã€‚

       ä¸Šè½®next=3,e=3,执行下一次循环可以发现,3.next=null,所以此轮循环将会是最后一轮循环。

       æŽ¥ä¸‹æ¥å½“执行完e.next=newTable[i]即3.next=7后,3和7之间就相互连接了,当执行完newTable[i]=e后,3被头插法重新插入到链表中,执行结果如下图所示:

       ä¸Šé¢è¯´äº†æ­¤æ—¶e.next=null即next=null,当执行完e=null后,将不会进行下一轮循环。到此线程A、B的扩容操作完成,很明显当线程A执行完后,HashMap中出现了环形结构,当在以后对该HashMap进行操作时会出现死循环。

       å¹¶ä¸”从上图可以发现,元素5在扩容期间被莫名的丢失了,这就发生了数据丢失的问题。

       3、JDK1.8中的线程不安全

       ä¸Šé¢çš„扩容造成的数据丢失、死循环已经在在JDK1.8中已经得到了很好的解决,如果你去阅读1.8的源码会发现找不到HashMap#transfer(),因为JDK1.8直接在HashMap#resize()中完成了数据迁移。

       ä¸ºä»€ä¹ˆè¯´ JDK1.8会出现数据覆盖的情况? æˆ‘们来看一下下面这段JDK1.8中的put操作代码:

       å…¶ä¸­ç¬¬å…­è¡Œä»£ç æ˜¯åˆ¤æ–­æ˜¯å¦å‡ºçŽ°hash碰撞,假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

       é™¤æ­¤ä¹‹å‰ï¼Œè¿˜æœ‰å°±æ˜¯ä»£ç çš„第行处有个++size,我们这样想,还是线程A、B,这两个线程同时进行put操作时,假设当前HashMap的zise大小为,当线程A执行到第行代码时,从主内存中获得size的值为后准备进行+1操作,但是由于时间片耗尽只好让出CPU,线程B快乐的拿到CPU还是从主内存中拿到size的值进行+1操作,完成了put操作并将size=写回主内存,然后线程A再次拿到CPU并继续执行(此时size的值仍为),当执行完put操作后,还是将size=写回内存,此时,线程A、B都执行了一次put操作,但是size的值只增加了1,所有说还是由于数据覆盖又导致了线程不安全。

       ä¸‰ã€å¦‚何使HashMap在多线程情况下进行线程安全操作?

       ä½¿ç”¨ Collections.synchronizedMap(map),包装成同步Map,原理就是在HashMap的所有方法上synchronized。

       ä¾‹å¦‚:Collections.SynchronizedMap#get()

public V get(Object key) {

          synchronized (mutex) {

              return m.get(key);

          }

       }

       å¤åˆ¶ä»£ç 

       å››ã€æ€»ç»“

       1、HashMap线程不安全原因:

       åŽŸå› ï¼š

       JDK1.7 中,由于多线程对HashMap进行扩容,调用了HashMap#transfer(),具体原因:某个线程执行过程中,被挂起,其他线程已经完成数据迁移,等CPU资源释放后被挂起的线程重新执行之前的逻辑,数据已经被改变,造成死循环、数据丢失。

       JDK1.8 中,由于多线程对HashMap进行put操作,调用了HashMap#putVal(),具体原因:假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

       æ”¹å–„:

       æ•°æ®ä¸¢å¤±ã€æ­»å¾ªçŽ¯å·²ç»åœ¨åœ¨JDK1.8中已经得到了很好的解决,如果你去阅读1.8的源码会发现找不到HashMap#transfer(),因为JDK1.8直接在HashMap#resize()中完成了数据迁移。

       2、HashMap线程不安全的体现:

       JDK1.7 HashMap线程不安全体现在:死循环、数据丢失

       JDK1.8 HashMap线程不安全体现在:数据覆盖

concurrenthashmap1.7和1.8的区别

       concurrenthashmap1.7和1.8的区别:

       整体结构:

       1.7:Segment + HashEntry + Unsafe;

       1.8: 移除Segment,使锁的粒度更小,Synchronized + CAS + Node + Unsafe。

       扩展资料

       1.7:先定位Segment,再定位桶,put全程加锁,没有获取锁的线程提前找桶的位置,并最多自旋次获取锁,超过则挂起。

       1.8:由于移除了Segment,类似HashMap,可以直接定位到桶,拿到first节点后进行判断,1、为空则CAS插入;2、为-1则说明在扩容,则跟着一起扩容;3、else则加锁put(类似1.7)

       1.7:跟HashMap步骤一样,只不过是搬到单线程中执行,避免了HashMap在1.7中扩容时死循环的问题,保证线程安全。

       1.8:支持并发扩容,HashMap扩容在1.8中由头插改为尾插(为了避免死循环问题),ConcurrentHashmap也是,迁移也是从尾部开始,扩容前在桶的头部放置一个hash值为-1的节点,这样别的线程访问时就能判断是否该桶已经被其他线程处理过了。

       1.7:很经典的思路:计算两次,如果不变则返回计算结果,若不一致,则锁住所有的Segment求和。

       1.8:用baseCount来存储当前的节点个数,这就设计到baseCount并发环境下修改的问题(说实话我没看懂-_-!)。