容器note

hashMap

put的过程

1,如果数组为null或⻓度为0,则初始化哈希表。
2,根据键值key计算hash值得到插⼊的数组的位置,如果该位置为
空,没有链表则直接插⼊。
3,如果该位置不为空,已经有节点了,则又分三种情况:
1)key相同,直接把原来的值覆盖掉(通过比较hash和key值);
2)如果是红⿊树,直接插⼊红⿊树
3)如果是链表,则从头到尾遍历链表,如果找到key相同的节点,替换掉,如果没有,则插⼊链表后⾯(1.8是插到链表尾部, 1.7是插到头部),然后判断如果链表长度大于8,则转化为红黑树。 ⼤于8也不⼀定树化,还要满⾜数组的⻓度大于等于最⼩树形化的阈值64,才能转换为红⿊树;如果⼩于64的话,则进⾏扩容resize。
4,插⼊后,判断hashMap⻓度是否达到极限值,如果达到则扩容。

为什么HashMap中个数超过8才转为红黑树

根据泊松公式计算的,链表长度达到8个元素的概率为0.00000006,几乎是不可能事件

计算数组下标

在1.8中, tab[i = (n - 1) & hash] 和 e.hash & (newCap - 1)  都是计算节点在数组中的下标。

在1.7中是用 indexFor(int h, int length)。

    /**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

数组的长度要为2的次幂。

扩展

首先创建一个原来数据2倍的新数组。

在1.7中把原table的Node放到新的table中,使用的是头插法,也就是说,新table中链表的顺序和旧列表中是相反的,在HashMap线程不安全的情况下,这种头插法可能会导致环状节点

在1.8中

如果是链表,
计算节点在table中的下标的方法是:hash&(oldTable.length*2-1),hash原来是和1111与运算,扩容后和11111与运输。如果hash从右往左数第5位是0,那么还是原来的位置,如果hash从右往左数第5位是1,则为旧下标加上旧数组的长度。

于是我们有了这样的结论:这新旧两次计算下标的结果,要不然就相同,要不然就是新下标等于旧下标加上旧数组的长度

用e.hash & oldCap(10000,没有减一),判断hash从右往左数第5位是0还是1,如果是0,表示位置相同,如果是1新下标等于旧下标加上旧数组的长度。分别用尾插法插到两个链表里,最后将链表移到新的数组里。

如果是红黑树,
第一步和链表类似,计算在新数组的下标位置,拆成两个链表。
第二步,如果链表的长度<=6,那还是链表;如果大于6,则重新转为红黑树。

HashMap是线程不安全的表现

#1.在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。

#2.在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。如果线程A和线程B同时进行put操作,刚好这两条不同的数据hash值与操作出来的数组下标一样,并且该位置数据为null,所以这线程A、B都会进入第6行代码中。假设一种情况,线程A进入后还未进行数据插入时挂起,而线程B正常执行,从而正常插入数据,然后线程A获取CPU时间片,此时线程A不用再进行hash判断了,问题出现:线程A会把线程B插入的数据给覆盖,发生线程不安全。

重写equal()时为什么也得重写hashCode()

为了保证两个值(key)相等时,hashCode也相等。

if (p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k))))
    e = p;

ConcurrentHashMap

1,判空: null直接抛空指针异常;
2,hash:计算h=key.hashcode;调⽤spread计算hash=(h ^(h >>>16))&
HASH_BITS;
3,无限循环,何时插⼊成功,何时跳出
1)若table为空,则初始化,仅设置相关参数;
2)计算当前key存放位置,即table的下标i=(n - 1) & hash;
若待存放位置为null, casTabAt⽆锁插⼊;
3)若是forwarding nodes(检测到正在扩容),则helpTransfer(帮助其扩
容);
4)插⼊位置⾮空且不是forward节点(不在扩容),即哈希碰撞了,将头节点上锁
(保证了线程安全):1,如果哈希桶是链表,则遍历整个链表,如果遇到hash值与
key值都与新节点⼀致的情况,只需要更新value值即可。否则依次向后遍
历,直到链表尾插⼊这个结点;若链表⻓度>8,则treeifyBin转树(Note:若length<64,直接tryPresize,两倍table.length;不转树)。2,如果是红黑树,用CAS锁进行插入。
4,addCount(1L, binCount)。
Note:
1、 put操作共计两次hash操作,再利⽤“与&”操作计算Node的存放位置。
2、 ConcurrentHashMap不允许key或value为null。
3、 addCount(longx, intcheck)⽅法:
①利⽤CAS快速更新baseCount的值;
②check>=0.则检验是否需要扩容; if sizeCtl<0(正在进⾏初始化或扩容操
作)【nexttable null等情况break;如果有线程正在扩容,则协助扩容】; else
if 仅当前线程在扩容,调⽤协助扩容函数,注其参数nextTable为null

    //获取数组指定下标位置的元素,
    static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }

    //用cas的方式去table的指定位置设置值,设置成功返回true
    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }

    //指定位置设置值
    static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
    }

红黑树

红黑树通过如下的性质定义实现自平衡:

  1. 节点是红色或黑色。
  2. 根是黑色。
  3. 所有叶子都是黑色(叶子是NIL节点)。
  4. 每个红色节的两个子节点必须是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
  5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点(简称黑高)。

有了上面的几个性质作为限制,即可避免二叉查找树退化成单链表的情况。但是,仅仅避免这种情况还不够,这里还要考虑某个节点到其每个叶子节点路径长度的问题。如果某些路径长度过长,那么,在对这些路径上的及诶单进行增删查操作时,效率也会大大降低。这个时候性质4和性质5用途就凸显了,有了这两个性质作为约束,即可保证任意节点到其每个叶子节点路径最长不会超过最短路径的2倍。原因如下:

当某条路径最短时,这条路径必然都是由黑色节点构成。当某条路径长度最长时,这条路径必然是由红色和黑色节点相间构成(性质4限定了不能出现两个连续的红色节点)。而性质5又限定了从任一节点到其每个叶子节点的所有路径必须包含相同数量的黑色节点。此时,在路径最长的情况下,路径上红色节点数量 = 黑色节点数量。最短路径的情况下,路径上都是黑色节点,最长路径长度为两倍黑色节点数量,也就是最短路径长度的2倍。

编辑更新

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值