【nacos源码关键点】【欧皇玩家源码】【手机linux系统源码】hashmap源码1.7

时间:2024-12-23 03:01:59 来源:安卓opencv 源码编译 编辑:java面试源码问题

1.concurrenthashmap1.8源码如何详细解析?
2.HashMap不明白?来看看这篇文章吧!
3.HashMap为什么不安全?
4.JDK成长记7:3张图搞懂HashMap底层原理!
5.我说HashMap初始容量是16,面试官让我回去等通知

hashmap源码1.7

concurrenthashmap1.8源码如何详细解析?

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

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

       对于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不明白?来看看这篇文章吧!

       面试中常遇问题:简单谈谈HashMap。

       通过四个问题来深入探讨HashMap。

       1. HashMap是什么?

       2. HashMap的底层数据结构?

       3. HashMap1.7与1.8的区别?

       4. HashMap的扩容机制?

       HashMap是Java集合,用于存储键值对,非线程安全。

       上图展示简易的HashMap结构:存储Key和对应的Value。

       注意,HashMap中Key唯一,不允重复。

       允许存储Null的Key和Value,但Null作为键只能有一个。

       初始容量为2的次幂,设置为时,实际容量为。

       putVal()源码显示,通过(n-1)&hash定位元素位置。

       Hash冲突解决:链表+红黑树结构。

       1.8中,当链表节点超过8个时,转为红黑树。

       红黑树节点小于等于6个时,转回链表。手机linux系统源码

       扩容机制:1.7死循环问题,1.8采用尾插法解决。

       正常扩容情况:找到元素,记录next节点,链接至扩容数组。

       并发扩容问题:线程一记录元素,线程二完成扩容。

       线程一执行扩容,记录元素位置,继续执行线程一的扩容。

       此时形成死循环。

       1.8中使用尾插法,解决并发问题,但数据丢失。

       扩容考虑:容量和加载因子,默认容量为,加载因子为0.。

       数组大小小于,链表长度超过8,也会进行扩容。

       每次扩容扩大原数组的两倍。

       以上内容,希望对理解HashMap有所帮助。

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线程不安全体现在:数据覆盖

JDK成长记7:3张图搞懂HashMap底层原理!

       一句话讲, HashMap底层数据结构,JDK1.7数组+单向链表、JDK1.8数组+单向链表+红黑树。

       在看过了ArrayList、LinkedList的底层源码后,相信你对阅读JDK源码已经轻车熟路了。除了List很多时候你使用最多的还有Map和Set。接下来我将用三张图和你一起来探索下HashMap的源码如何算补码底层核心原理到底有哪些?

       首先你应该知道HashMap的核心方法之一就是put。我们带着如下几个问题来看下图:

       如上图所示,put方法调用了putVal方法,之后主要脉络是:

       如何计算hash值?

       计算hash值的算法就在第一步,对key值进行hashCode()后,对hashCode的值进行无符号右移位和hashCode值进行了异或操作。为什么这么做呢?其实涉及了很多数学知识,简单的说就是尽可能让高和低位参与运算,可以减少hash值的冲突。

       默认容量和扩容阈值是多少?

       如上图所示,很明显第二步回调用resize方法,获取到默认容量为,这个在源码里是1<<4得到的,1左移4位得到的。之后由于默认扩容因子是0.,所以两者相乘就是扩容大小阈值*0.=。之后就分配了一个大小为的Node[]数组,作为Key-Value对存放的数据结构。

       最后一问题是,如何进行hash寻址的?

       hash寻址其实就在数组中找一个位置的意思。用的算法其实也很简单,就是用数组大小和hash值进行n-1&hash运算,这个操作和对hash取模很类似,只不过这样效率更高而已。hash寻址后,就得到了一个位置,可以把key-value的Node元素放入到之前创建好的Node[]数组中了。

       当你了解了上面的三个原理后,你还需要掌握如下几个问题:

       还是老规矩,看如下图:

       当hash值计算一致,比如当hash值都是时,Key-Value对的Node节点还有一个next指针,会以单链表的形式,将冲突的天书奇谈私服源码节点挂在数组同样位置。这就是数据结构中所提到解决hash 的冲突方法之一:单链法。当然还有探测法+rehash法有兴趣的人可以回顾《数据结构和算法》相关书籍。

       但是当hash冲突严重的时候,单链法会造成原理链接过长,导致HashMap性能下降,因为链表需要逐个遍历性能很差。所以JDK1.8对hash冲突的算法进行了优化。当链表节点数达到8个的时候,会自动转换为红黑树,自平衡的一种二叉树,有很多特点,比如区分红和黑节点等,具体大家可以看小灰算法图解。红黑树的遍历效率是O(logn)肯定比单链表的O(n)要好很多。

       总结一句话就是,hash冲突使用单链表法+红黑树来解决的。

       上面的图,核心脉络是四步,源码具体的就不粘出来了。当put一个之后,map的size达到扩容阈值,就会触发rehash。你可以看到如下具体思路:

       情况1:如果数组位置只有一个值:使用新的容量进行rehash,即e.hash & (newCap - 1)

       情况2:如果数组位置有链表,根据 e.hash & oldCap == 0进行判断,结果为0的使用原位置,否则使用index + oldCap位置,放入元素形成新链表,这里不会和情况1新的容量进行rehash与运算了,index + oldCap这样更省性能。

       情况3:如果数组位置有红黑树,根据split方法,同样根据 e.hash & oldCap == 0进行树节点个数统计,如果个数小于6,将树的结果恢复为普通Node,否则使用index + oldCap,调整红黑树位置,这里不会和新的容量进行rehash与运算了,index + oldCap这样更省性能。

       你有兴趣的话,可以分别画一下这三种情况的图。这里给大家一个图,假设都出发了以上三种情况结果如下所示:

       上面源码核心脉络,3个if主要是校验了一堆,没做什么事情,之后赋值了扩容因子,不传递使用默认值0.,扩容阈值threshold通过tableSizeFor(initialCapacity);进行计算。注意这里只是计算了扩容阈值,没有初始化数组。代码如下:

       竟然不是大小*扩容因子?

       n |= n >>> 1这句话,是在干什么?n |= n >>> 1等价于n = n | n >>>1; 而|表示位运算中的或,n>>>1表示无符号右移1位。遇到这种情况,之前你应该学到了,如果碰见复杂逻辑和算法方法就是画图或者举例子。这里你就可以举个例子:假设现在指定的容量大小是,n=cap-1=,那么计算过程应该如下:

       n是int类型,java中一般是4个字节,位。所以的二进制: 。

       最后n+1=,方法返回,赋值给threshold=。再次注意这里只是计算了扩容阈值,没有初始化数组。

       为什么这么做呢?一句话,为了提高hash寻址和扩容计算的的效率。

       因为无论扩容计算还是寻址计算,都是二进制的位运算,效率很快。另外之前你还记得取余(%)操作中如果除数是2的幂次方则等同于与其除数减一的与(&)操作。即 hash%size = hash & (size-1)。这个前提条件是除数是2的幂次方。

       你可以再回顾下resize代码,看看指定了map容量,第一次put会发生什么。会将扩容阈值threshold,这样在第一次put的时候就会调用newCap = oldThr;使得创建一个容量为threshold的数组,之后从而会计算新的扩容阈值newThr为newCap*0.=*0.=。也就是说map到了个元素就会进行扩容。

       除了今天知识,技能的成长,给大家带来一个金句甜点,结束我今天的分享:坚持的三个秘诀之一目标化。

       坚持的秘诀除了上一节提到的视觉化,第二个秘诀就是目标化。顾名思义,就是需要给自己定立一个目标。这里要提到的是你的目标不要定的太高了。就比如你想要增加肌肉,给自己定了一个目标,每天5组,每次个俯卧撑,你看到自己胖的身形或者海报,很有刺激,结果开始前两天非常厉害,干劲十足,特别奥利给。但是第三天,你想到要个俯卧撑,你就不想起床,就算起来,可能也会把自己撅死过去......其实你的目标不要一下子定的太大,要从微习惯开始,比如我媳妇从来没有做过俯卧撑,就让她每天从1个开始,不能多,我就怕她收不住,做多了。一开始其实从习惯开始,先变成习惯,再开始慢慢加量。量太大养不成习惯,量小才能养成习惯。很容易做到才能养成,你想想是不是这个道理?

       所以,坚持的第二个秘诀就是定一个目标,可以通过小量目标,养成微习惯。比如每天你可以读五分钟书或者5分钟成长记,不要多,我想超过你也会睡着了的.....

       最后,大家可以在阅读完源码后,在茶余饭后的时候问问同事或同学,你也可以分享下,讲给他听听。

我说HashMap初始容量是,面试官让我回去等通知

       HashMap是工作和面试中常见的数据类型,但很多人只停留在会用的层面,对它的底层实现原理并不深入理解。让我们一起深入浅出地解析HashMap的底层实现。

       考虑以下面试问题,你能完整回答几个呢?

       1. HashMap的底层数据结构是什么?

       JDK1.7使用数组+链表,通过下标快速查询,解决哈希冲突。JDK1.8进行了优化,引入了红黑树,查询效率提升到O(logn)。在JDK1.8中,数组+链表+红黑树结构,当链表长度达到8,并且数组长度大于时,链表会转换为红黑树。

       2. HashMap的初始容量是多少?

       在JDK1.7中,初始容量为,但在JDK1.8中,初始化时并未指定容量,而是在首次执行put操作时才初始化容量。初始化时仅指定了负载因子大小。

       3. HashMap的put方法流程是怎样的?

       源码揭示了put方法的流程,包括哈希计算、桶定位、插入或替换操作等。

       4. HashMap为何要设置容量为2的倍数?

       为了更高效地计算key对应的数组下标位置,当数组长度为2的倍数时,可以通过逻辑与运算快速计算下标位置,比取模运算更快。

       5. HashMap为何线程不安全?

       因为HashMap的所有修改方法均未加锁,导致在多线程环境下无法保证数据的一致性和安全性。例如,一个线程删除key后,其他线程可能还无法察觉,导致数据不一致;在扩容时,另一个线程可能添加元素,但由于没有加锁,元素可能丢失,影响数据安全性。

       6. 解决哈希冲突的方法有哪些?

       常见的方法包括链地址法、线性探测法、再哈希法等。

       7. JDK1.8扩容流程有何优化?

       JDK1.7在扩容时会遍历原数组,重新哈希,计算新数组下标,效率较低。而JDK1.8则优化了流程,只遍历原数组,通过新旧数组下标映射减少操作,提高了效率。

       推荐阅读:《我爱背八股系列》

       面试官问关于订单ID、分库分表、分布式锁、消息队列、MySQL索引、锁原理、查询性能优化等八股文问题时,幸亏有总结的全套八股文。

       以上内容是关于HashMap的深入解析和面试常见问题的解答,希望能够帮助到大家。

copyright © 2016 powered by 皮皮网   sitemap