并发编程系列——5HashMap核心原理分析

学习目标

  1. hash冲突的解决办法有哪几种

  2. HashTable、hashmap、CHM三者之间的区别

  3. HashMap的默认长度是多少?默认扩容因子是多少?

  4. HashMap它是怎么解决hash冲突的

  5. HashMap为什么扩容是2的幂次方

  6. 谈一下HashMap中put是如何实现的?

  7. 谈一下hashMap中什么时候需要进行扩容,扩容resize()又是如何实现的?

  8. 谈一下hashMap中get是如何实现的?

  9. 为什么是16?为什么必须是2的幂?如果输入值不是2的幂比如10会怎么样?

  10. HashMap和HashTable的区别

  11. hashmap中的扰动函数有什么作用

  12. hashmap的底层数据结构(1.7和1.8版本)

  13. hashmap为什么是线程不安全的(1.7和1.8版本)

  14. 在什么情况下hashmap的链表会转化成红黑树

  15. HashMap的key可以为null吗?CHM呢?

第1章 前置知识

1、Hash表​​​​​​​​​​​​​​​​​​​​​

Hash函数:MD5、SHA

Hash表:通过hash函数来计算数据位置的数据结构

HashMap通过数组去存储

2、Hash冲突

多个不同的key通过hash函数运算之后落在同一个数组下标的位置。

解决方案:

  • ThreadLocal解决方案:线性探索(开发寻址法)

  • HashMap解决方案:链式地址法

  • 再Hash法(通过多个hash函数)

3、锁粒度

  • hashtable:是对整个数组加锁,两个线程对同一个hashtable进行操作时,只可能有一个线程进行put。

  • 1.7CHM:对数组中的每个segment加锁,不同线程对同一个CHM进行操作时,如果hash计算没有落在同一个segment时,就可以同时操作。每个segment里面存的也是一个16个长度的数组,然后数组里面存链表

  • 18CHM:对数组中的每个node加锁,不同线程对同一个CHM进行操作时,如果hash计算没有落在同一个node时,就可以同时操作。每个node里面存的是存链表或红黑树。

第2章 HashMap

2.1 从new说起

HashMap hashMap=new HashMap(5);
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);  //代表hashMap的容量大于这个的时候会进行resize操作
}

 不管传多大的容量,容量都是最靠近设置值的2的幂次方,且大于传入值

2.2 右移左移补充

<<表示左移移,不分正负数,低位补0; 

注:以下数据类型默认为byte-8位

左移时不管正负,低位补0

正数:r = 20 << 2

  20的二进制补码:0001 0100

  向左移动两位后:0101 0000

       结果:r = 80

r = 20 << 3

  20的二进制补码:0001 0100

  向左移动两位后:1010 0000

                         原码:1110 0000

       结果:r = -96

负数:r = -20 << 2

  -20 的二进制原码 :1001 0100

  -20 的二进制反码 :1110 1011

  -20 的二进制补码 :1110 1100

  左移两位后的补码:1011 0000

        反码:1100 1111

        原码:1101 0000

        结果:r = -80

>>表示右移,如果该数为正,则高位补0,若为负数,则高位补1;

注:以下数据类型默认为byte-8位

正数:r = 20 >> 2

  20的二进制补码:0001 0100

  向右移动两位后:0000 0101

       结果:r = 5

负数:r = -20 >> 2

  -20 的二进制原码 :1001 0100

  -20 的二进制反码 :1110 1011

  -20 的二进制补码 :1110 1100

  右移两位后的补码:1111 1011

        反码:1000 0100

        原码:1000 0101

        结果:r = -5

>>>表示无符号右移,也叫逻辑右移,即若该数为正,则高位补0,而若该数为负数,则右移后高位同样补0

正数: r = 20 >>> 2

    的结果与 r = 20 >> 2 相同;

负数: r = -20 >>> 2

注:以下数据类型默认为int 32位

  -20:源码:10000000 00000000 00000000 00010100

    反码:11111111 11111111 11111111 11101011

    补码:11111111 11111111 11111111 11101100

    右移:00111111 11111111 11111111 11111011

    结果:r = 1073741819

-1源码: 10000000 00000000 00000000 00000001

反码: 11111111 11111111 11111111 11111110

补码: 11111111 11111111 11111111 11111111

            01111111 11111111 11111111 11111111

            11111111 11111111 11111111 11111111

//保证
static final int tableSizeFor(int cap) {
    int n = cap - 1;  //减1.防止传入的刚好是2的幂次方的时候,比如传入4,不减1,会得到7;
    n |= n >>> 1;  //右移一位,然后|运算,能保证高2位是1,是4                                                         n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;   //类推到最大的2的32-1,也就是32个1!!因为java中int4字节,也就是32位,达到int的最大值
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;  //再加1,
}

举例 传入的cap是14  

n=cap-1 ;  13   二进制位0000 1101
    
n |= n >>> 1;    n>>>1 为 0000 0110   与n|运算    得到00001111
n |= n >>> 2;    n>>>2 为 0000 0011   与n|运算    得到00001111
n |= n >>> 4;    n>>>4 为 0000 0000   与n|运算    得到00001111
最终结果就是 00010000

2.3 扰动函数

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

右移16位与本来的异或;能使高16位与低16位都参与hash运算!!减少hash冲突

比如

1110 1000  >>>4   0000 1110
0000 1110    
---------------
1110 0110    

2.4 继续put方法

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node[] tab; Node p; int n, i;
    //判断table是否为空
    if ((tab = table) == null || (n = tab.length) == 0)
        //初始化
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)   //不存在hash冲突
        tab[i] = newNode(hash, key, value, null);
    else {    //hash冲突
        Node e; K k;
        if (p.hash == hash &&  //hash值相等
            ((k = p.key) == key || (key != null && key.equals(k))))  //key也相等
            e = p;  //直接将已有的给到e
        else if (p instanceof TreeNode) //是红黑树
            e = ((TreeNode)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); //添加到链表尾部
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st   //当链表长度达到8,转变为红黑树
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))   //hash跟key都一样,链表里面覆盖
                    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)
        resize();
    afterNodeInsertion(evict);
    return null;
}

2.5 扩容(初始化)

final Node[] resize() {   
    Node[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;  //得到原来table的长度.假如是4
    int oldThr = threshold;  //得到原来的扩容临界值   4*0.75=3
    int newCap, newThr = 0;  //新的table的长度与扩容临界值 
    if (oldCap > 0) {  //如果table有值,代表不是初始化   
        if (oldCap >= MAXIMUM_CAPACITY) {   //判断原来的table长度是不是到了最大值,如果到了最大值,不再扩容,直接返回原来的数组
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&   //newCap为oldCap的2倍,并且原来的长度>=16,临界值为原有临界值的2倍,这时newCap=8
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold,结果是跟newCap*0.75一样的,必须大于等于16,不然4的扩容是3,*2就会有问题
    }
    else if (oldThr > 0) // initial capacity was placed in threshold  //当没有数据的时候,初始化容量就为阈(yu)值
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults   //默认值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {  //当原来table的长度小于16的时候
        float ft = (float)newCap * loadFactor;   //newCap为原来长度的2倍*0.75     也就是8*0.75=6 
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);    //新的扩容临界值为6
    }
    threshold = newThr;  //扩容值变成了6
    @SuppressWarnings({"rawtypes","unchecked"})   //不用再编译完成后出现警告信息,忽略"rawtypes","unchecked"
        Node[] newTab = (Node[])new Node[newCap];  // 新的数组长度为8
    table = newTab;     //table赋值为newTab
    if (oldTab != null) {  //进行数据迁移
        for (int j = 0; j < oldCap; ++j) {
            Node e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;   //便于GC
                if (e.next == null)  //没有next,代表不是链表,就是数组
                    newTab[e.hash & (newCap - 1)] = e;   //hash取模放到数组位置
                else if (e instanceof TreeNode)  //如果是红黑树,按红黑树转移
                    ((TreeNode)e).split(this, newTab, j, oldCap);
                else { // preserve order   //链表,链表转移
                    //低位链表,也就是当hash取模小于old的时候
                    Node loHead = null, loTail = null;
                    //高位链表,也就是当hash取模大于等于old的时候
                    Node hiHead = null, hiTail = null;
                    Node next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    //低位链表放在原有位置
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    //如果超过了原来old,放在old的长度+j
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

2.6 链表转红黑树

final void treeifyBin(Node[] tab, int hash) {
    int n, index; Node e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)   //数组不到64,也不会转红黑树
        resize();  //扩容
    else if ((e = tab[index = (n - 1) & hash]) != null) {  
        TreeNode hd = null, tl = null;   // 定义首、尾节点
        do {
            TreeNode p = replacementTreeNode(e, null);  //将该节点转换为 树节点
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);  // 继续遍历链表
        
          // 到目前为止 也只是把Node对象转换成了TreeNode对象,把单向链表转换成了双向链表
        
        if ((tab[index] = hd) != null)
            hd.treeify(tab);  //转为红黑树
    }
}

你可能感兴趣的:(并发编程,java,哈希算法,缓存,链表)