hashmap转红黑树条件(hashmap转红黑树的条件)
jdk1.8的hashmap真的是大于8就转换成红黑树,小于6就变成链表吗
本文夹杂部分笔者个人观点,如描述有误,欢迎指正
写这篇文章,是因为最近研究hashmap源码的时候,会结合网上的一些博客来促进理解。而关于红黑树和链表相互转换这一块,大部分的文章都会这样描述:hashmap中定义了两个常量:
当链表元素个数大于8的时候,就会转换为红黑树;当红黑树元素个数小于6的时候,就会转换回链表。
笔者通过仔细观察,发现这种说法并不严谨。hashMap中确实定义了这两个常量,但并非简单通过元素个数的判断来进行转换。
链表转换为红黑树的最终目的,是为了解决在map中元素过多,hash冲突较大,而导致的读写效率降低的问题。在源码的putVal方法中,有关红黑树结构化的分支为:
即网上所说的,链表的长度大于8的时候,就转换为红黑树,我们来看看treeifyBin方法:
可以看到在treeifyBin中并不是简单地将链表转换为红黑树,而是先判断table的长度是否大于64,如果小于64,就通过扩容的方式来解决,避免红黑树结构化。原因呢?笔者个人觉得链表长度大于8有两种情况:
第二种情况是可以用扩容的方式来避免的,扩容后链表长度变短,读写效率自然提高。另外,扩容相对于转换为红黑树的好处在于可以保证数据结构更简单。
由此可见并不是链表长度超过8就一定会转换成红黑树,而是先尝试扩容
基本思想是当红黑树中的元素减少并小于一定数量时,会切换回链表。而元素减少有两种情况:
hashMap的remove方法,会进入到removeNode方法,找到要删除的节点,并判断node类型是否为treeNode,然后进入删除红黑树节点逻辑的removeTreeNode方法中,该方法有关解除红黑树结构的分支如下:
可以看到,此处并没有利用到网上所说的,当节点数小于UNTREEIFY_THRESHOLD时才转换,而是通过红黑树根节点及其子节点是否为空来判断。而满足该条件的最大红黑树结构如下:
节点数为10,大于 UNTREEIFY_THRESHOLD(6),但是根据该方法的逻辑判断,是需要转换为链表的
resize的时候,判断节点类型,如果是链表,则将链表拆分,如果是TreeNode,则执行TreeNode的split方法分割红黑树,而split方法中将红黑树转换为链表的分支如下:
这里才用到了 UNTREEIFY_THRESHOLD 的判断,当红黑树节点元素小于等于6时,才调用untreeify方法转换回链表
红黑树详解
首先,我们来了解一下二叉查找树,二叉查找树具备以下几个特点:
1、左子树上所有节点的值均小于或等于它的根节点的值;
2、右子树上所有节点的值均大于或等于它的根节点的值;
3、左右子树也分别为二叉排序树。
下面我们以一棵典型的二叉查找树来查找值为10的节点:
以上图例正是典型的二分查找的思想,查找所需的最大次数等同于二叉查找树的高度。在往树中插入新节点的时候也要用类似的方法,通过一层一层地比较大小从而找到新节点适合插入的位置。但是即便如此,二叉查找树依旧存在它的缺陷,并且此缺陷恰恰体现在插入新节点的时候。请看下面图例展示:
这样的瘸腿形态虽然也符合二叉查找树的特性,但是查找的性能却大打折扣,几乎变成了线性数据结构。为了解决二叉查找树多次插入新节点而导致的不平衡问题,红黑树便应运而生了。
红黑树是一种自平衡的二叉查找树,除了符合二叉查找树的特性外,还具有下列性质:
1、根节点是黑色,节点是红色或黑色;
2、每个叶子节点都是黑的空节点;(nil节点)
3、每个红色节点的两个子节点都是黑色;(也就是说从每个叶子到根的所有路径上不能有两个连续的红色节点)
4、从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
这些规则的限制,保证了红黑树的平衡,红黑树从根到叶子的最长路径不会超过最短路径的两倍。当红黑树插入或者删除节点的时候,红黑树的规则可能被打破,这时候就需要做出调整来维持它的平衡了。请看下面的例子(注意:新插入的节点必须是红色,否则就没有意义了):
由于父节点22是也是红色节点,因此打破了红黑树的规则(每个红黑树的两个子节点都是黑色),所以必须进行调整,使之重新符合规则。那么我们需要作出怎样的调整才能保证一棵红黑树始终是符合规则的标准红黑树呢?调整有两种方法:“变色”和“旋转”,其中,旋转又分为两种形式:“左旋转”和“右旋转”。
为了重新符合红黑树的规则,尝试把红色节点变成黑色,或者把黑色节点变成红色。
下图是摘自上面红黑树的一部分,节点25并非根节点。正如上面所说因为新节点21和节点22连续出现了红色,不符合规则,所以把节点22从红色变成黑色。
但这样并不算完,节点22变成黑色后,凭空多出的黑色节点又打破了规则,发生连锁反应,需要继续把节点25从黑色变为红色。
此时仍未结束,节点25变为红色后,又和节点27形成了两个连续的红色,规则又被打破,需要继续把节点27从红色变为黑色。
如此一路走下来,完成变色调整。
逆时针旋转红黑树的两个节点,使得父节点被自己的右孩子取代,而自己成为左孩子。
顺时针旋转红黑树的两个节点,使得父节点被自己的左孩子取代,而自己成为右孩子。
这几种方法究竟怎么使用呢?红黑树的插入和删除包含很多种情况,每种情况都有不同的处理方式,下面举一个典型的例子,可以体会一下这个过程。
还以刚才插入节点21为例:
首先我们变色处理,把节点25及其下方的节点变色:
此时节点17和节点25是连续的两个红色节点,那么此时再把节点17变成黑色节点可以吗?显然是不可以的,这样依然不符合规则,更不可以把根节点13变成红色。
既然变色已经无法解决问题,我们此时就要使用旋转了,我们把节点13看作X,把节点17看作Y,进行左旋转:
旋转完成后,由于根节点必须是黑色,所以还需要进行变色:
至此并没有结束,因为其中两条路径(17-8-6-NIL)的黑色节点数是4,其他路径的黑色节点数是3,依旧不符合规则。这时我们需要把节点13看成X,节点8看成Y,做右旋转:
然后再进行变色:
经过上面一系列的调整,我们的红黑树终于变得重新符合规则,整个过程有点复杂,经历了:变色--左旋转--变色--右旋转--变色这样的一系列步骤。
经过上面细致的步骤演示,想必大家对二叉查找树和红黑树有所了解了吧,对红黑树结构插入新节点所触发的一系列的变化流程也有了大概印象。实际中红黑树的应用是很多的,比如JDK(Java开发工具包)的集合类TreeMap和TreeSet底层就是红黑树实现的,在Java8中,HashMap也用到了红黑树。其实关于红黑树自平衡的调整,插入和删除节点时涉及到的情形一一展开讲解还是很多很多的,但是万变不离其中,红黑树自平衡调整的主体思想都是上面所叙述的,大家有兴趣可以再进行深入的探究。
HashMap多线程不安全问题总结
? ? 1、HashMap的底层数据结构是数组+链表的结构。在插入数据的时候,会先计算数据的hashcode值,再取余放入数组对应下标处。如果发生hash碰撞,则插入当前node的后面,形成一个链表的结构。
2、数组的默认大小是16,默认的扩容因子是0.75,每次达到阈值( size * 0.75)的时候,就会对数组进行扩容,扩容每次都是在现有基础上扩容2倍。
3、在数组长度大于64,并且链表长度大于8的时候,链表会转成红黑树,之所以是需要数组长度大于64,是因为优先要扩容数组大小,减少hash碰撞的次数,提高性能。在红黑树小于7的时候,会转成链表,之所以是要小于7,是避免红黑树和链表之间的频繁转换。
了解到了HashMap的主要特点,再来看HashMap的线程不安全问题:
? ? ? ? ?在线程A、B put的数据发生hash碰撞并且数组里面已经有数据的情况下,两个线程同时获得头结点的next指针,next指针只能指向一条数据,比如有另外一条数据丢失。
? ? ? ? 在数组扩容的时候,会new一个新的数组出来,再遍历旧的数组,如果从旧数组里面取出来的值非null,会将这个值取出来放到新数组里面,并将旧数组里面的值置为null,而这个时候,旧数组还没有遍历完全,另外一个线程get数据的时候可能获取到的时候就是null。
一图了解ConcurrentHashMap底层原理
1、ConcurrentHashMap底层数据结构是一个数组table
2、table数组上挂着单向链表或红黑树
3、new ConcurrentHashMap();如果没有指定长度的话,默认是16,并且数组长度必须是2的n次幂,若自定义初始化的长度不是2的n次幂,那么在初始化数组时,会吧数组长度设置为大于自定义长度的最近的2的n次幂。(如:自定义长度为7,那么实际初始化数组后的长度为8)
4、底层是使用synchronized作为同步锁,并且锁的粒度是数组的具体索引位(有些人称之为分段锁)。
5、Node节点,hash0,当hash冲突时,会形成一个单向链表挂在数组上。
6、ForwardindNode,继承至Node,其hash=-1,表示当前索引位的数据已经被迁移到新数组上了
7、ReservationNode,继承至Node,其hash=-3,表示当前索引位被占用了(compute方法)
8、TreenBin,继承至Node,其hash=-2,代表当前索引下挂着一颗红黑树
9、lockState为ConcurrentHashMap底层自己实现的基于cas的读写锁,锁粒度是具体的某颗树。当向红黑树进行增,删操作时,首先会先上sync同步锁,然后自平衡的时候会上lockState写锁,当读红黑树的时候,会上lockState读锁,然后判断此时是否有线程正持有写锁,或是否有线程正在等待获取写锁,若有,则读线程会直接去读双向链表,而不是去读红黑树。
10、TreeNode,hash0,为红黑树具体节点。TreeBin的root代表红黑树的根节点,first代表双向链表的第一个节点
11、双向链表:构建红黑树时还维护了一个双向链表,其目的为:(1)当并发写读同一颗红黑树的时候,读线程判断到有线程正持有写锁,那么会跑去读取双向链表,避免因为并发写读导致读线程等待或阻塞。(2)当扩容的时候,会去遍历双向链表,重新计算节点hash,根据新的hash判断放在新数组的高位还是地位
1、触发扩容的规则:
1)添加完元素后,若判断到当前容器元素个数达到了扩容的阈值(数组长度*0.75),则会触发扩容
2)添加完元素后,所在的单向链表长度=8,则会转为红黑树,不过在转红黑树之前会判断数组长度是否小于64,若小于64则会触发扩容
2、table为扩容前的数组,nextTable为扩容中创建的新数组,迁移数据完毕后需要将nextTable赋值给table
3、扩容后的数组是元素组长度的2倍
4、ln,hn分别表示高位和低位的链表(高链,低链)。即,扩容时,会用nh==0来判断元素应该放在新数组的i位置还是i+n位置。n:元素组长度;h:元素hash值;i:元素所在的原数组索引位;。这样就会出现有些元素会被挂在低位,有些元素会被挂在高位,从而达到打散元素的目的。
5、红黑树扩容时,会遍历双向链表,并且计算nh==0来判断元素放在低位(lo)还是高位(ho),确定完元素的去处之后,会判断分别判断两个双向链表(lo,ho)的长度是否大于6,若大于6则将还是以一颗红黑树的结构挂在数组上,若=6的话,则转为单向链表,挂在数组上