对HashMap和concurrentHashMap的理解

一、HashMap1.7原理的细节

1、hashMap的重新rehash,他会有个哈希种子,防止自己重写hashcode方法导致这个算出来的hash值不够散,所以会出现根据一个哈希种子做一系列的运算得到一个新的hash值。什么时候触发呢,需要在jvm配置参数-Djdk.map.althashing.threshold=?,这个就是阈值,当你数组的length超过这个大小在进行hash的时候就会进行rehash

2、hashmap有modcount当版本号,每次修改都会增加,当一个线程a在遍历map,线程b去改了会导致modcount变化,如果不是他期望的modcount值,a就会出现并发修改异常。

二、HashMap1.8设计的比较好的地方

1、在put进来会先判断table是不是空,如果为空就初始化数组。源码里面table是个放在堆中的不是局部变量,而且后面需要用多次,所以设计者直接把这个table赋值给了个put方法点局部变量,让他存放在栈帧中,这样可以利用jvm的特性提高put性能。

2、每次初始化或者扩容完,都会用大小*负载因子得到下次扩容到阈值,存起来,方便后面的比较

3、1.8算hash并不是直接拿key算,而是先拿key算出一个值a,再把key右移动16位hash算出b(他一共32位的,右移了一半),然后a和b异或得出的hash值,这样可以让前面一半和后面一半都能参与到后面和length的&运算

4、treeNode是node的子类,他除了有left和right属性,还有pre属性指向上级节点,还有继承父类的next节点,所以他可以是个双向链表。当node满足条件变为treeNode的时候,就是遍历每个node变为treeNode然后pre指向上一个节点,形成一条双向链表,方便后面树化

5、hash出来发现是链表要追加到尾部的时候,不仅仅是遍历到为null就添加到尾部,其中还包其他操作,每次遍历到一个节点都要比较是否相等,相等就直接改value了。当遍历发现节点有8个,就会先加入其中变9个再转红黑树,转红黑树的方法会判断是否数组长度超64没有会先数组扩容(因为扩容也会减少链表的长度)

6、在进行扩容时,如果是链表他会生成个低位节点和高位节点,然后分别去指向链表中不同的节点,最终遍历完会拆分成两条链表进行扩容,一条放新数组的旧位置,一条放新位置。

7、进行扩容时,如果是树节点,也是去遍历他分别有个高位节点和低位节点去指向,最后生成高位链表和低位链表,如果有个链表为null,那么直接把原来的treeNode移动过去;如果不是则分开移动超过8会生成新的treeNode,小于等于6就退化回链表(退化是很简单的,treeNode变node只是少了3个属性,原本的next还是在的)

三、concurrentHashMap1.7的小细节

1、并发级别指的是segment的个数,默认的segment大小是16,segment的大小最大就是16,而且必须也是2的幂次方,因为插入也要先&找出在哪个segment。 每个segment里面又有数组,然后他会再根据segment的长度再算具体在哪个位置。Segment的长度在第一次初始化后就确定了,后面是不会改变的

2、segment对象用的锁是reentrantLock

3、Put的时候是先用hash对segment数组大小&算出哪个segment,如果那个位置没有segment对象就构建,这个构建是cas操作来的,构建是用原型模式,他直接用最开始初始化好的segmemt的参数直接复用,就不用重新计算(在初始化数组的时候就会生成出一个segment对象放到0号位置,然后后面都用这个,数组的大小是初始化/segment数量,然后向上取整的2的幂次方,比如33的初始化大小16个segment,数组大小为4)

4、put方法是先获取reentrantlock的,如果没有就会重新尝试,重试过程中并不是什么都不做就等,而且会去判断需不需要new一个hashEntry对象,如果需要会new一个(会遍历链表看看有没有和key相等的,没有就new,如果new了就切换重试的逻辑,重试的时候就不执行这个代码了执行检查的代码遍历看遍表有木有改变),如果遍历的时候发现链表发生了改变,就会重新执行一遍new HashEntry的逻辑,看看是不是要重新new一个,因为链表元素改变了可能有影响;每次重试都会增加重试次数,当达到一定次数就会阻塞了。

5、扩容是看segment内部的数组来扩容的,当某个segment对象里面的数组超过了阈值,先会在内部重新搞个2倍的来扩容,局部的扩容,所以每个segment对象内的数组长度是可能不一样的。(局部segment的扩容,如果你的一个segment内部超过了阈值就会扩容)

四、concurrentHashMap1.8的小细节

put是先是个循环里面很多分支:

  1. 如果数组为空,走初始化数组的方法,里面是while循环自选看初始化了没,没就cas初始化
  2. 如果下标位置为null就cas去创建node节点,如果cas没成功再次循环也不会走这个if条件了;
  3. 还有个if是判断一个枚举f是不是-1,如果是代表正在扩容,会执行帮忙锁节点的方法;
  4. else就加sync锁去put元素,里面又有两个分支:链表的插入和红黑树的插入,如果是链表就遍历链表有相等的就修改,没有就在末尾添加,中间会有个变量随着遍历++,最后比较一下是否到阈值到了就走树化方法;如果是树就调用红黑树的插入方法(关于红黑树这边,不是用的之前的treeNode而且重新new了个TreeBin来当根节点,因为如果用hashmap那种方式直接treenode来当根他是直接根节点上锁的,当红黑树发生左旋右旋的时候,根节点就发生变化了,所以不能用原来的方式,现在根就一直用treebin,里面随便怎么变,只要获取到treeBin的锁就行,treeBin就是红黑树,里面有个属性是root就是红黑树的跟节点)

每次put会有个baseCount记录数量,看是否超过阈值的,这个是用cas去增加的,如果cas成功万事大吉,不成功就会用LongAddr那个类差不多的,他会用随机数&length-1找到对应cell数组的下标,增加自己私有的累加器

多线程扩容是每个线程去移动自己的桶不会一起移动一个,然后他整个扩容逻辑是写在while里面的,因为可能刚扩容完又要扩容了又put到阈值了。

每次移动完一个桶就会把那个桶改为fwd,如果put的时候发现桶为fwd说明正在扩容,他就会帮助转移元素,当扩容完成了就会进入下次循环,然后就能拿到新的数组,这个时候如果不是fwd就可以直接走put了

扩容流程:

1、根据操作系统的 CPU 核数和集合 length 计算每个核一轮处理桶的个数,最小是16

2、修改 transferIndex 标志位,每个线程领取完任务就减去多少,

比如初始大小是transferIndex = table.length = 64,每个线程领取的桶个数是16,

第一个线程领取完任务后transferIndex = 48,也就是说第二个线程这时进来是从第 48 个桶开始处理,再减去16,依次类推,这就是多线程协作处理的原理

3、领取完任务之后就开始处理,如果桶为空就设置为 ForwardingNode ,

如果不为空就加锁拷贝,只有这里用到了 synchronized 关键字来加锁,为了防止拷贝的过程有其他线程在put元素进来。

拷贝完成之后也设置为 ForwardingNode节点。

4、如果某个线程分配的桶处理完了之后,再去申请,发现 transferIndex = 0,

这个时候就说明所有的桶都领取完了,但是别的线程领取任务之后有没有处理完并不知道,

该线程会将 sizeCtl 的值减1,然后判断是不是所有线程都退出了,如果还有线程在处理,就退出,

直到最后一个线程处理完,发现 sizeCtl = rs<< RESIZE_STAMP_SHIFT 也就是标识符左移 16 位,才会将旧数组干掉,用新数组覆盖,并且会重新设置 sizeCtl 为新数组的扩容点。

你可能感兴趣的:(哈希算法,算法,java,哈希表,HashMap)