hashmap-随笔

yuque.com/renyong-jmovm/kb
****************************JDK 1.7
头插法和尾插法其实都进行了循环遍历,只不过头插法是部分遍历而尾插法是要遍历整个链表,
hashmap在初始化容量的时候,如果初始化大小<16则默认为16,如果>16的话会初始化2的倍数,也就是说是32,他是通过int.higestOnebit((x-1)<<1)
异或运算 不相同则为1
hashmap中的hash方法是对key的hashCode进行一系列的右移和异或运算,使得最终的hash值更散列,减少冲突。
为什么要是2的倍数,因为只有是2的倍数,在寻址算法中才能用与运算

扩容:只有hashmap中的size>容量*0.72 并且 当前index位置不为空时才会扩容
rehash主要是考虑到当前的hash算法不够散列,在JVM中设置jdk.map.althashing.threshold,当capacity>这个值时,就会进行rehash,
调用的方法是initHashSeedASNeeded

如果hashmap大小是固定的,可以在初始化的时候设置容量和对应的加载因子,两者运算出来的预值>固定大小 
就可以防止多线程操作hashmap扩容时因头插法而引起的循环链表问题

fast-fail 快速失败容错机制,主要时为了解决多个线程 一个遍历,另一个在put或者remove操作。
如果允许这种情况,可以调用hashmap自己的removeEntryForKey(K)方法。

ConcurrentHashMap:
Segment[] table; 继承了ReentrantLock
HashEnty
在初始化的时候,segment和hashEnty都是2的倍数,每个segment下面挂载的hashEnty= initialCapacity/concurrencyLevel,这两个值默认都是16,
在初始化后的segment下的HashEnty在初始化后的segment下的HashEnty[]是一样大的,
同时将第一个segment初始化放在hashmap的第一个位置,这样做的原因是在后续的put元素,不用再重新计算HashEnty[]的长度。
每个segment下的hashEnty在扩容的时候,只是对自己进行扩容,不会影响其他segment下的hashEnty,是局部扩容。

多个线程对segment的操作使用的是while和Unsafe类比较与交换进行创建,
而segment的put,remove对HashEnty[]的操作用的是ReentrantLock,但是对HashEnty[]中的HashEnty的操作又是unsafe类
!!scanAndLockForPut!!在segment的put代码里首先就是加锁,但是这里的加锁机制是while,在没有获取到锁的时候创建一个node(k,value),但是这个while是由次数的
retries和cpu有关(单核为1,多核为64),当重试次数大于64的时候就加锁lock()。在这个过程中由可能其它线程也进行了put操作,此时的头结点
已经改变,这时需要将retries重置为-1,此时会重新执行上面的逻辑,最终会有两种结果,一是加锁并返回预先创建的node,另一种是获取到了锁(也就加锁成功)并返回null。
rehash扩容的时候有点类似蜘蛛纸牌,lastRun直到最后节点,这些hashEnty是存放在相同的新数组的节点 一下子都移动过去,
然后再遍历头结点到lastRun进行一个一个的头插法转移。
最后将新节点头插法插入到新数组的位置
GET方法就是根据hash值和unsafe.getObjectVolatile找到对应的segment;然后遍历HashEnty[]根据hash值和unsafe.getObjectVolatile找到对应的HashEnty
Size获取的时候是先进行两次不加锁的统计,如果统计结果不对(是由于中间还有其他线程在操作map,导致两次统计结果不一致),则对每个segment进行加锁,然后统计segment个数(size)和每个segment中的HashEnty数量(count)

Unsage:
Field field = Unsafe.class.getDeclaredField(“theUnsafe”);
field.setAccessible(true);
Unsafe UNSAFE = (Unsafe) field.get(null);
long offset = UNSAFE.objectFieldOffset(MyQuene.class.getDeclaredField(“size”));

UNSAFE.compareAndSwapInt(stack, offset,stack.size, stack.size +1);

****************************红黑数
几个规则

几种情况:
1.如果父节点是黑色,不需要处理,直接将新增的红色节点加入对应的位置
2.如果父节点为红色
2.1叔叔节点为红色,则将父节点和叔叔节点变色(黑色),祖父节点变色(红色)【如果此时的曾祖父节点也为红色,则继续递归执行这一步骤】
2.2叔叔节点为空或者为黑色,则对祖父节点进行左旋或者右旋,然后将父节点变为黑色,曾祖父节点变为红色【也有左旋之后接着右旋的情况】

****************************JDK 1.8
在put的时候,计算hash值,找到地址,会进行三种情况的判断
1.如果当前位置为空,说明是第一个节点,直接赋值
2.如果不为空
2.1 判断这个位置上的key是否和新增的key相同,相同的话覆盖这个位置的数据
2.2 判断这个位置上的数据是否为TreeNode
2.3 此时这个位置肯定挂载的是一个链表,遍历链表,进行尾插法,在这个过程中如果遍历链表的次数达到8的时候就转为红黑树
【如果table.length小于64的时候是不会进行树化的】,其实此时链表的长度是9

双向链表,TreeNode有parent,left,right,prev,red这几个属性,同时TreeNode继承了LinkedHashMap.Entry,
LinkedHashMap.Entry又继承了HashMap.Node这个Node中有next属性,这样就变成了一个双向链表。

get的时候需要判断第一个节点的key相同则直接返回;否则判断这个first节点数据类型,遍历链表或者红黑树(递归find)
resize
remove过程中如果操作的是红黑树,遍历找到key对应的TreeNode,首先要通过改变指针的方式移除双向链表中该节点,如果此时满足一定的条件就会将
红黑树转为单链表;否则遍历红黑树移除这个TreeNode

****************************JDK 1.8VS 1.7
resize的时候直接判断size>threshold就进行扩容,1.7还要判断table[index]不为空才会扩容。
在操作链表进行扩容的时链表数据移动的过程中,1.8将链表中的数据进行高低数据位的分组,然后移动到新扩容的数组中,这样避免了循环整个链表进行移动
这也是扩容必须是2的倍数的原因【将hash值与扩容大小进行与,为0的话就是就是存放在之前就的位置上,为1的话就是存放在index+旧数组大小】
红黑树数据(本身也是一个维护了前后节点的双向链表)的转移也是区分高地位存放的,如果低位的链表的长度小于等于6 就对这个红黑树转化为单链表
如果大于6,并且高位的链表为空的话,不用进行树化,直接将低位链表赋值给低位。如果高位链表不为空,则需要将低位链表进行树化。高位链表的操作和
低位链表执行机制是一样的。

ConcurrentHashMap,jdk1.8版本中没有了segment分段锁的概念
PUT,如果tab为null或者长度为0,进行初始化,通过while(table==null)和使用cas(SIZECTL–>0–>-1–扩容的预值)进行初始化;
如果key寻址后的位置上没有数据,使用cas新增数据;
如果key对应的数组位置的头个元素的hash值为MOVED(-1),说明有线程对数据进行扩容,则帮助进行扩容;
如果index位置有数据,加synchronize对数据进行操作。如果是链表则遍历链表,key相同则更新,不相同则使用尾插法插入数据,
当链表的遍历次数等于8的时候就进行树化(这个过程是单独抽出来一个方法,里面使用synchronize,这里使用的TreeBin,是new出来的一个对象,没有直接使用TreeNode是因为
多线程操作一个TreeNode,它的头节点可能会发生变化,不能对这个变化的头节点加锁);如果是红黑树,则遍历添加这个节点
addCount这个操作就是对baseCount或者对CounterCell中的value加1,保证每次操作都会把1累加起来。
然后通过s = sumCount()进行统计baseCount和CounterCell[]中的value之和,接着用这个值来判断是否可以扩容。

addCount扩容:
1. 尝试对BASECOUNT进行CAS加1,成功最后计算s = sumCount(),用来判断是否达到扩容的预值
失败则使用当前线程生成一个随机数,对CounterCell[]进行寻址,找到对应的CounterCell;对CounterCell进行CAS加1操作,
s = BASECOUNT + CounterCell[]中所有CounterCell.value之和。

2. while进行自选(因为有可能会有多个线程进行put操作而引发同时扩容,扩容完成之后会获取到新的newTab),其中根据sc是否小于0进行判断,小于0说明其他线程正在进行扩容,然后所在线程帮助转移。
对sc进行unsafs的cas操作设置为-1,设置成功则进行数据转移,转移成功的话会将这个节点设置为fwd对象,advance为true,继续向前帮助转移,如果转移完毕finish为true;
否则再次进入自选执行上面的逻辑。【finish为true说明整个转移过程执行完了,否则继续计算要转移的位置进行帮助转移】

3. 真正要对数据转移的时候要加上sync锁,链表使用的是蜘蛛牌和遍历lastRun进行比较(hash&新数组长度),为0则为ln追加节点,为1则为hn追加节点
红黑树的话和hashmap差不多,首先遍历双向链表,根据(hash&新数组长度)进行分组loTail,hiTail;
接着判断loTail,hiTail≤6则退化为链表,否则进行树化,如果loTail,hiTail其中有一个为空,也就是说对应的计数器为0,则直接给响应节点赋值为原红黑树TreeBin.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值