HashMap 需要关注的点

1. Map 原理解析

1.1. key 的计算过程

要将其存入到HashMap中,就需要确定key,value组成的Node对象在数组索引下标中的位置。

Map中数组初始大小为16,即索引位置为0-15

将key转换成数据中的索引位置:

  1. 求key的hashCode值,
    任何对象都可以计算得出hashCode值,hashCode()方法是Object中的方法,结果返回一个int值
public native int hashCode();
  1. 将计算得出的进行取模运算,hashCode % 16,即可以得到对应数中的索引位置

但是直接对一个数进行取模运算并不高效,可以将数值转换成二进制,然后进行&运算,这样更高效
示例:

"java".hashCode(); //结果返回3254818

将3254818装换成二进制,然后与15(length-1)的二进制01111进行 & 运算,可以很高效的计算得出结果为8。
任何一个二进制数值与01111进行 & 运算,最大值为1111,最小值为0000。换算成十进制,最大值为15,最小值为0,结果和直接与16进行取模运算一样。

11 0010 0101 0100 1000 0001 1000
00 0000 0000 0000 0000 0000 1111    &
00 0000 0000 0000 0000 0000 1000    结果为8

根据上述规则,产生一个新的问题是,如果两个key不相同,但是求得hashCode的值换算成二进制时最后4位是相同的,那么最终得到的索引位置是相同的。这样位置就会冲突。

期望:不同的key计算得到的索引尽可能是不一样的。
根据上述进行 & 运算的规则,可以影响计算出的index结果的只有key.hasCode计算得到的值的最后几位,那么就需要让参与 & 运算的规则最后几位的值尽可能的不一样。
结合key.hasCode返回的结果是int型,int转换成二进制一定是一个0-32位之间的一个数,因此将求得的hashCode值一分为二,拆成高16位和低16位,然后将高16位和低16位进行 ^(异或) 运算得到一个新的结果,然后再参与 & 运算,这样计算得到的索引值就会尽可能的减低重复率。即,key.hashCode() ^ key.hashCode() >>> 16

0000 0011 0010 0101 
0100 1000 0001 1000     ^
0100 1011 0011 1101

将求得的hashCode进行高低16位的异或操作后,在计算得到的索引势必还是会重复,这种情况下,只能按链表和红黑树进行存储。

1.2. 数组长度的限制

按照 & 运算的规则,想要得出的结果只受参与运算的ope1决定,那么ope2必须全是1才行。
ope2=length-1,那么length必须是最高位是1,其余位全是0才行,因此length必须是2的n次幂才行。

因此数组的长度一定是2的n次幂

1.3. put 源码解析

在put的时候,共需要以下几个步骤

  1. 对key进行hash计算,得到数组中的index
  2. 如果数组未初始化,则先进行数组初始化
  3. 判断当前inedx位置是否为null,如果为null,则创建Node并方式数据
  4. 如果不为null,判断是否以链表的方式存储
  5. 判断是否已红黑树方式存储
  6. 当数组中存储的数据超过一定范围时,就对数组进行扩容

注意:
链表的长度超过8,就转为红黑树;红黑树里的节点小于6,就转为链表。
在扩容时要保证2的倍数扩容,比如16 —> 32,符合2的幂次的规律。

public V put(K key, V value) {
    //先对key进行hashCode运算,得到index值
    return putVal(hash(key), key, value, false, true);
}


final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        //如果数据没有初始化,则进行数据初始化
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        //根据计算得到的索引,判断当前位置有没有值,如果没有,则放入当前位置
        tab[i] = newNode(hash, key, value, null);
    else {
        /*
        对应索引的位置已经有元素了,初步想法是分为三种情况:
        1.如果key相同,则直接将索引位置的元素替换;
        2.key不相同,想要以链表的方式存储
        2.key不相等,想要以红黑树的方式存储
        */
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //情况1,key相同,hash值相同,进行替换
            e = p;
        else if (p instanceof TreeNode)
            //红黑树
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            //链表
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null); //单项链表,尾部插入发,Node节点的形成
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        //链表的长度如果太长,存储和查找性能就会大大降低,因此链表的长度要有限制,超过了长度限制后,就会将链表转化成红黑树
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        //1.当数组的长度超过一定限制后,就会进行扩容。
        //2.扩容完成后,还要进行重新散列,即将老数组中的数据移动到新的数组
        //threahold=数据长度*0.75
        resize();
    afterNodeInsertion(evict);
    return null;
}

为什么数组下面存储元素,要先以链表的方式进行存储,当链表长度超过8之后,就会将链表转换成红黑树?为什么不直接使用红黑树?
导致问题的根因就是,对构建链表和红黑树的空间、时间复杂度的取舍。

链表和红黑树出现的本质是:数组、链表、红黑树的性能不同。

2. HashMap非线程安全

HashMap想要线程安全最简单的方式就是给put方式加上synchronize关键字。而HashTable就是这样做的。
给整个put方法加上锁的话,当一个线程去put一个值的时候,其余线程再put值的时候就只能等待。但是可能线程1和线程2操作的并不是同一个索引位置的数据。这样直接全部锁住不太合理。那么具体该如何操作呢?
ConcurrentHashMap已经帮我们实现了这种操作。

ConcurrentHashMap在put的时候,首先保证了数组初始化的线程安全。

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            //第一个线程在执行数组初始化之前,调动sun.misc.Unsafe类的compareAndSwapInt方法通过乐观锁(cas)的机制保证初始化数组的线程安全
            try {
                if ((tab = table) == null || tab.length == 0) {
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

当index==null时,同样采用cas的方式保证了线程安全;
当index!=null是,由于链表、红黑树结构的变化,继续采用cas的方式保证线程安全的话,需要比较的次数就会增大,造成性能的浪费。

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            //在进行put值的时候,当index==null时,也是通过cas的机制保证线程安全
            if (casTabAt(tab, i, null,
                            new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            //MOVED 当有线程在进行数组扩容时,会被置为-1,然后其余线程进行put时,检测到该值就会帮助其进行扩容
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            //当index!=null时,通过加锁的方式来保证线程安全。锁对象是当前数组index位置的对象,这样就保证了index位置下面链表或红黑树操作的安全性。对于其他index位置不受影响
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                    (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                            value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                        value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

扩容是一定会有线程安全的问题的,因为扩容时进行了两步操作

  1. 创建新数组
  2. 将老数组中的元素移动到新数组中

当线程1进行扩容时,其余线程是不能进行put的,除线程不安全外,即使put了以后,在线程1扩容之后,也会重新改变index。但是如果其余线程等待的话,就需要干等。于是ConcurrentHashMap进行了如下设计:

  1. 由最先收到扩容请求的线程进行扩容,通过java.util.concurrent.ConcurrentHashMap#transfer方法的传值不同实现该过程。
  2. 其他线程一起参与数组数据的移动,将数组按照16划分,然后按照划分好的区间领取任务,对区间内的数据进行移动
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值