【JavaSE】HashMap源码分析(jdk8)

概述

HashMap本质是一个hash表,采用拉链法解决冲突,并且在某个位置的链表过长时将其转化为一棵二叉搜索树降低插入的开销;

按照以下顺序去阅读HashMap的源码是不错的选择

  • 构造函数
  • put( )方法
  • remove( )方法
  • get( )方法

一、主要成员

1.1 成员属性

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

    private static final long serialVersionUID = 362498820763181265L;

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 初始化时map容器的最大容量:16

    static final int MAXIMUM_CAPACITY = 1 << 30;

    static final float DEFAULT_LOAD_FACTOR = 0.75f; // 哈希表的装载因子

    static final int TREEIFY_THRESHOLD = 8; // 哈希表每一个位置的链表转化为红黑树的临界长度值

    static final int UNTREEIFY_THRESHOLD = 6; // 红黑树需要恢复链表时,树结点个数的临界值
    
     // 某个链表个数 > 树化的阈值之后,树化之前会判断table的总长度大于MIN_TREEIFY_CAPACITY才会转化为红黑树
     // 若此时 table的总长度小于MIN_TREEIFY_CAPACITY则会进行一次扩容
    static final int MIN_TREEIFY_CAPACITY = 64;
    
    transient Node<K,V>[] table; // HashMap的本体哈希表
    
    final float loadFactor; // 装载因子(hash表中 (已有元素容量 / 总容量) > 装载因子时会扩容) 
    
    int threshold; // threshold = capacity * loadFactor, 当元素个数 > threshold需要进行扩容
    
    transient int size; // 元素个数
    ...
    ...
    ...
}

1.2 主要函数

public V put(K key, V value) {
       ...
    }

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
       ...
      
    }

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

final Node<K,V> getNode(int hash, Object key) {
       ...
    }

1.3 内部类

// 链表结点
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next; // 指向下一个结点的指针

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
		...
        ...
        ...
     
    }

// 树结点
 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev
        ...
        ...
        ...
 }
   

通过这些内部成员可以看出实际上HashMap底层结构应该是这样:

在这里插入图片描述

table数组每一个位置一般称为"桶",每个桶最开始为null,随着元素的插入变为链表,单个链表元素超过一定阈值会进行树化变成红黑树

1.4 构造函数

一般情况下分为指定初始capacity和不指定的情况

不指定初始容量时

仅将装载因子设置为默认值,不做创建工作

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

指定初始容量

除了设定装载因子之外,会根据输入的初始容量确定一个容量,其为大于初始给定容量的,最小的2的整数次幂

public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

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;
    	// 指定cap时,并未直接设定capacity值而是设定threshold值,之后resize()中会按照这个值创建table数组
        this.threshold = tableSizeFor(initialCapacity); 
    }
/**
取大于cap的,最小的2的整数次幂作为容量
*/
static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

补充:

tableSizeFor(int cap)是取得大于等于cap的最小的2的整数次幂的函数;

原理:对于一个二进制数来说,获得比自己大的最小的2的整数次幂的方法是将最左边(高位)的1之后的所有位都变成1,然后+1,变成1000…这样的形式;

比如12 换成二进制为 00001100,将最左边的1之后的所有位都变为1,即 00001111,+1变为 00010000即十进制的16;

tableSizeFor(int cap)就是让初始二进制分别右移1,2,4,8,16位,与自己异或实现这样一个过程

第一次:n |= n>>>1 将n中最高位和最高位之后的那一位变为1,

第二次:n |= n>>>2 利用第一次的结果,最高的前两位已经是1,为了加快后面的1的变化速度,直接一次性将前4位都变成1

后面类似

实际上也可以这么写:

一位一位去转化,单是要右移31次,时间开销更大一些

public int tableSizeFor(int cap) {
        int n = cap - 1;
        int i = 0;
        while(i < 31) {
            n |= n >>> 1;
            i++;
        }
        return n + 1;
    }

二、put( )

2.1 无冲突,无扩容

这种情况下,直接插入元素即可

test()函数:

@Test
public void test() {
    HashMap<Integer, String> mp = new HashMap<>();
    mp.put(1, "hhh");
}

new HashMap<>( )会进入空参构造函数:

由于未指定装载因子,采用默认装载因子(16)

此时不会创建Map底层的Hash表,只有在后续进行**put( )**操作的时候才会创建,是一种懒加载的方式

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; 
}
  • 下面执行put( ),方法内部会执行putVal( ):
public V put(K key, V value) {
    	// 将key-value添加到hash表中
        return putVal(hash(key), key, value, false, true);
    }

// 计算key的hash值(使用key对象内部的hashCode通过一定计算得出,并不会直接使用)
static final int hash(Object key) {
        int h;
    	// (h = key.hashCode()) ^ (h >>> 16)是将hashCode右移16位后与原hashCode按位相或取得hash值(这样可以获取更好的散列性能)
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 
    }

这里的代码

(h = key.hashCode()) ^ (h >>> 16)

称做扰动函数,可以将h的高低位特征尽量保留下来,使得hash结果更加分散,散列性能更优

  • 下面执行putVal( )(创建table数组的过程):

    • 省略此时不执行的代码
    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; // 第一次put()时会创建table(resize()中)
            ...
            ...
            ...
        }
    
    // 此时进行创建table的工作
    final Node<K,V>[] resize() {
            Node<K,V>[] oldTab = table; // 此时为null
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            int oldThr = threshold;
            int newCap, newThr = 0;
            if (oldCap > 0) {
                ...
            }
            else if (oldThr > 0) // initial capacity was placed in threshold
                newCap = oldThr; // 创建map时如果指定了cap则oldThr会有值,这个值是大于等于指定cap的最小的2的整数次幂
            else {
                // 第一次建立table初始化参数
                newCap = DEFAULT_INITIAL_CAPACITY; // 容量
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // threshold值
            }
            if (newThr == 0) {
                ...
            }
            threshold = newThr;
            @SuppressWarnings({"rawtypes","unchecked"})
                Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 创建table数组
            table = newTab;
            if (oldTab != null) {
                ...
            }
            return newTab;
        }
    
    
  • 下面执行putVal( )(真正的插入数据):

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) // i为应该放入的table数组中的下标, n为数组容量
            tab[i] = newNode(hash, key, value, null); // 放入
        else { // 第一次插入数据不会进else,进else只有在出现冲突时 即 p != null
            ...
        }
        ...
        if (++size > threshold) // 如果map中总的元素个数超过threshold会进行扩容
            resize();
        afterNodeInsertion(evict);
        return null;
    }

这里解释下tab[i = (n - 1) & hash]获取插入位置的下标操作之所以可以成功,是因为n是table数组的容量,这个值一定会是2的整数次幂,所以 hash除n取余的操作和 (n - 1) & hash的值相等

即 hash % n == (n - 1) & hash在n为2的次幂的时候成立

之所以桶的长度一定为2的n次幂,还有一个原因是按照上述方法计算地址,如果二进制最后一位是1, 在 - 1后会是0,这样无论任何hash值与其按位相与结果都只会是偶数(末位为0),这样就只会映射到偶数位置的桶,浪费一半空间

2.2 有冲突,无扩容

与2.1的情况下,仅putVal( )执行的分支不同

在源码中将出现冲突分为以下两种(i为计算hash后的到的插入位置下标):

  • 元素重复,即key值完全相同
  • 仅hash后的地址(下标)冲突

重复的定义是满足如下条件之一:

key:待插入的key值

k:为原链表上某个元素的k值

  • key == k
  • key != null && key.equals(k) == true

如果仅仅是地址冲突,则会遍历到table[i]处链表的末尾插入,这里可以看到JDK 8插入元素使用尾插法

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) // p == null说明没有发生冲突
            tab[i] = newNode(hash, key, value, null); 
    	
        else { // 出现冲突, table已经存在
            Node<K,V> e; K k; // e指向链表下一个元素,p 和 e交替赋值完成遍历链表的动作
            // p指向table[i]处链表第一个元素
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k)))) // 第一个元素和待插入元素一样
                e = p;
            else if (p instanceof TreeNode) // 已经转化为树
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 插入树结点
            else { // 到此说明,table[i]处第一个元素的key和待插入的元素的key不同
                for (int binCount = 0; ; ++binCount) {  // 遍历链表到尾部
                    if ((e = p.next) == null) { // 找到尾部,并插入
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // 链表长度是否超过需要树化的阈值
                            treeifyBin(tab, hash); // 链表转化为树
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k)))) // 在链表中发现相同元素break
                        break;
                    p = e;
                }
            }
            if (e != null) { // e不为空必然是出现key值重复
                V oldValue = e.value;
                // key值重复时是直接覆盖还是插入失败看参数"onlyIfAbsent",这是构造函数传入的值,默认为false即重复插入直接覆盖
                if (!onlyIfAbsent || oldValue == null) 
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ...
        ...
        ...
    }

2.2.1 树化

下面看看树化的代码:

treeifyBin(Node<K,V>[] tab, int hash)

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
    	// 树化前要先判断table的长度是否大于等于MIN_TREEIFY_CAPACITY,不满足则用一次扩容取代本次的树化
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) { // e指向需要树化的"桶"
            TreeNode<K,V> hd = null, tl = null;
            // 下面的do-while循环,将原链表转化为双向链表(为了以后解除树化恢复链表)之后再做树化
            // 过程中会新建TreeNode而不是使用原结点
            do {
                // hd -> 链表头,tl -> 链表尾部
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            
            // 将双向链表转化为树
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
        return new TreeNode<>(p.hash, p.key, p.value, next);
    }

 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev
        ...
        ...
        ...
 }
            

所以其实树化是这么一个过程:

在这里插入图片描述

2.3 扩容

在插入元素之后如果发现map容器中若所有元素的总数量 > threshold 时 会进行扩容,增加table的长度(桶的数量),目的是为了减少冲突提高性能 ,其中threshold = capacity * 装填因子;

扩容时,一般是capacity * 2即 table数组长度 * 2,会新建一个是原数组长度两倍的数组然后将原数组中的数据转移过去,”搬“的过程会涉及两个问题

  • 搬到哪?即放到新数组哪个位置
  • 需要变化吗?即原位置可能是链表或者树,搬过去之后需要树化或者解除树化吗
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    	// 插入操作
        ...
        ...
        ...
        // 扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }


 final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 扩容一般是直接capacity * 2, threshold * 2
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            ...
        }
        if (newThr == 0) { // 确定新的扩容阈值
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
     	// 开始扩容操作
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) { // 遍历原数组 table
                Node<K,V> e;
                if ((e = oldTab[j]) != null) { // e为每个桶的链表的head或者树的root
                    oldTab[j] = null; // 原数组置空(这样虚拟机才会进行垃圾回收)
                    if (e.next == null) // 该处桶仅有一个元素
                        newTab[e.hash & (newCap - 1)] = e; // 解决问题1:搬到哪,仍旧是按照hash % cap的方式放到新的table中
                    else if (e instanceof TreeNode) // 此处是树结点需要单独处理
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // 转移红黑树
                    else { // 链表转移
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        // do-while将链表分为两部分
                        // 链表1:头指针hiHead,尾指针hiTail,元素的特征是(e.hash & oldCap) != 0
                        // 链表2:头指针loHead,尾指针loTail,元素的特征是(e.hash & oldCap) == 0
                        // 实际上是因为这条链表的元素转移过后所处的newTable的位置可能会不同
                        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;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

这里解释下为什么转移时要把同一个链表的结点区分为两种结点,拆分成两个链表?

因为元素转移之后,hash表的映射规则不会变,但是由于hash表容积变大(2倍),原来映射到同一个"桶"的元素(比如下标为index)可能映射到新的hash表中的下标为index或者 index + oldTable.length的位置(可以用数学方法证明);

因此将链表的结点分成两类结点一类结点映射到newTable的index位置(低位),另外一类映射到 index + oldTable.length的位置(高位),能够映射到哪个位置取决于其hash值与 oldTable.length按位相与的结果是否为0(即代码所写的 if ((e.hash & oldCap) == 0) )

树的转移:

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
            TreeNode<K,V> b = this;
            // Relink into lo and hi lists, preserving order
            TreeNode<K,V> loHead = null, loTail = null;
            TreeNode<K,V> hiHead = null, hiTail = null;
            int lc = 0, hc = 0;
    		// 利用维护的双向链表分成高位/低位两条链表(前文有解释过)
            for (TreeNode<K,V> e = b, next; e != null; e = next) {
                next = (TreeNode<K,V>)e.next;
                e.next = null;
                if ((e.hash & bit) == 0) {
                    if ((e.prev = loTail) == null)
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;
                    ++lc;
                }
                else {
                    if ((e.prev = hiTail) == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;
                    ++hc;
                }
            }
			
    		// 低位链表
            if (loHead != null) {
                if (lc <= UNTREEIFY_THRESHOLD)
                    tab[index] = loHead.untreeify(map); // 低位链表元素个数过少,解除树的结构退化成链表
                else {
                    tab[index] = loHead;
                    if (hiHead != null) // (else is already treeified)
                        loHead.treeify(tab);
                }
            }
    
    		// 高位链表
            if (hiHead != null) {
                if (hc <= UNTREEIFY_THRESHOLD)
                    tab[index + bit] = hiHead.untreeify(map);
                else {
                    tab[index + bit] = hiHead;
                    if (loHead != null)
                        hiHead.treeify(tab);
                }
            }
        }

// 利用双向链表结构退化成链表
final Node<K,V> untreeify(HashMap<K,V> map) {
    Node<K,V> hd = null, tl = null;
    for (Node<K,V> q = this; q != null; q = q.next) {
        Node<K,V> p = map.replacementNode(q, null);
        if (tl == null)
            hd = p;
        else
            tl.next = p;
        tl = p;
    }
    return hd;
}

看了源码可以回答之前的两个问题:

  • 搬到哪(原下标 index)

    • hash & oldCap == 0 -> 新下标 为 index
    • hash & oldCap != 0 -> 新下标 为 index + oldCap
  • 搬得过程需要变化吗

    • 如果原位置是链表,转移后仍然是链表
    • 如果原位置是树,分为高位,低位链表后如果元素个数 < UNTREEIFY_THRESHOLD则退化成链表

2.4 putVal( )线程不安全

HashMap是线程不安全的类,其中putVal( )也是一个线程不安全的方法,高并发情况下可能会出现数据覆盖的情况

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        ...
        ...
        ...
        if ((p = tab[i = (n - 1) & hash]) == null) // 如果没有hash冲突则直接插入元素
            tab[i] = newNode(hash, key, value, null);
        ...
        ...
        ...
      	if (++size > threshold)
            resize();
    }

上面两处代码存在线程安全问题

  • 在插入元素时如果没有发生hash冲突就会直接在链表尾部直接插入元素;

    如果线程A判断没有冲突准备插入时,时间片耗尽,线程B同样执行该处代码,判断没有冲突(因为线程A还没有插入元素),插入了元素,然后线程A开始插入元素就会直接覆盖线程B的元素;

    如果发生了冲突,在尾插入链表的时候出现切换线程执行相同操作,在高并发情况下同样的问题也会发生,可以自行分析。

  • 插入完元素后需要判断是否需要扩容,会进行++size操作,这个操作并不是原子操作,可能会发生以下情况

    • 线程A从内存拿到size的值,假设为10;
    • 线程B占用CPU也拿到size的值10,并在CPU中做+1操作,然后写回主存,此时主存size = 11
    • 线程A继续执行,使用CPU内部寄存器中保存的size值10,做+1操作,结果为11,然后写回主存直接覆盖之前线程B修改的值
    • 最终size = 11 显然不对

JDK 8 中不仅上述的地方线程不安全,几乎所有方法里都有不安全的部分,并发安全的HashMap类叫ConcurrentHashMap,源码解读可以看下面

三、get()

get()方法很好理解,根据hash找到对应的位置,如果是链表则按序遍历;

如果是树则按照如下规则查找:

1 获取当前节点, 第一次进入是根节点;
2 通过一个循环去遍历节点, 当下一个遍历节点为空时退出, 返回null;
3 先比较每个节点的hash值, 目标节点的hash小于当前节点时, 位于当前节点的左子树, 将当前节点的左子节点赋值给p, 进行下一次循环;
4 目标节点的hash大于当前节点时, 位于当前节点的右子树, 将当前节点的右子节点赋值给p, 进行下一次循环;
5 如果目标节点的hash等于当前节点的hash值, 再比较key是否相同, 如果相同则返回节点; 不相同则进入下面的步骤
6 左节点为空时, 将右节点赋值给p, 进行下一轮循环;
7 右节点为空时, 将左节点赋值给p, 进行下一轮循环;
8 如果key不相等, 再次判断能否通过key::compareTo(key是否实现Comparable接口)比出大小, 如果小, 则将左子节点赋值给p, 如果大则将右子节点赋值给p, 进行下一轮循环;
9 如果无法通过key::compareTo比较出大小, 右子节点递归调用find, 如果结果不为空, 则返回结果(第8步已经保证pr不为空了);
10 如果右子节点的递归无法得出结果, 只能将左子节点赋值给p, 进行下一轮循环;

HashMap中的红黑树除了按照hash值排序外,第二排序参考值是key的大小(通过key:::compareTo比较得出,key没有实现则按照默认方式)

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key); // 树中查找
                do { // 链表中查找
                    if (e.hash == hash && 
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }


final TreeNode<K,V> getTreeNode(int h, Object k) {
            return ((parent != null) ? root() : this).find(h, k, null);
        }


// 红黑树中查找类似在BST中查找(红黑树按照hash值为排序规则)
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
            TreeNode<K,V> p = this;
            do {
                int ph, dir; K pk;
                TreeNode<K,V> pl = p.left, pr = p.right, q;
                if ((ph = p.hash) > h)
                    p = pl;
                else if (ph < h)
                    p = pr;
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    return p;
                else if (pl == null)
                    p = pr;
                else if (pr == null)
                    p = pl;
                else if ((kc != null ||
                          (kc = comparableClassFor(k)) != null) &&  		// hash相等时按照key的大小决定遍历方向
                         (dir = compareComparables(kc, k, pk)) != 0)
                    p = (dir < 0) ? pl : pr;
                else if ((q = pr.find(h, k, kc)) != null)
                    return q;
                else
                    p = pl;
            } while (p != null);
            return null;
        }

四、ConcurrentHashMap

读懂ConcurrentHashMap源码前先要理解几个重要属性的含义:

4.1 重要属性

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {
    
    private transient volatile long baseCount; // 元素个数基值(计算元素个数用baseCount +  counterCells中所有值)

    private transient volatile int sizeCtl; // 极为重要见下方详解

    private transient volatile int cellsBusy; // 指示counterCells是否正在扩容或者新建

    private transient volatile CounterCell[] counterCells; // 计算元素个数的工具数组

    transient volatile Node<K,V>[] table;

    private transient volatile Node<K,V>[] nextTable; // 扩容时的临时数组
}


  • sizeCtl

可以说最为重要的一个变量

sizeCtl为0,代表数组未初始化, 且数组的初始容量为16

sizeCtl为正数,如果数组未初始化,sizeCtl的是数组的初始容量,如果数组已经初始化,sizeCtl是数组的扩容阈值

sizeCtl为-1,表示数组正在进行初始化

sizeCtl小于0,并且不是-1,表示数组正在扩容, -(1+n),表示此时有n个线程正在共同完成数组的扩容操作

4.2 源码解读

  • 构造函数
public ConcurrentHashMap() {
    }

public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap; // 直接设置sizeCtl为参数(容量值)
    }

4.2.1 table的初始化

在put时也会先进行table的初始化

final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
    	// spread内部和HashMap计算hash方法基本一样,只是最后将结果与7FFFFFFF进行了位与运算,保证hash为正数(负数hash有其他用)
        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();
            ...
            ...
            ...
    }
    
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(); // 并发安全,如果sizeCtl < 0 则表示其它线程正在初始化或者正在扩容,当前线程暂停
            // CAS操作尝试修改sizeCtl为-1,类似于获取锁的过程,修改成功则表示当前线程可以进行创建table工作
            // SIZECTL是通过线程安全的方法获取到的sizeCtl变量所在的地址,到此说明为正数可以进行table初始化,其值代表初始容量
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        // sizeCtl > 0且未初始化,则这个值代表初始容量(如果创建map时传入初始容量值则此时sc > 0)
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY; 
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        //  n >>> 2 = n / 4
                        // 此时sc(即sizeCtl) 为 0.75 * n,sizeCtl > 0且table已创建时sizeCtl代表的时扩容的阈值
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc; // 数组创建
                }
                break;
            }
        }
        return tab;
    }

对比HashMap创建table的代码:

// 此时进行创建table的工作
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table; // 此时为null
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            ...
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr; // 创建map时如果指定了cap则oldThr会有值,这个值是大于等于指定cap的最小的2的整数次幂
        else {
            // 第一次建立table初始化参数
            newCap = DEFAULT_INITIAL_CAPACITY; // 容量
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // threshold值
        }
        if (newThr == 0) {
            ...
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 创建table数组
        table = newTab;
        if (oldTab != null) { // 如果oldTab == null就是第一次初始化table,下面的转移数据过程不会执行
            ...
        }
        return newTab;
    }

可以看到HashMap判断是不是初始化table数组的判断依据仅仅是 oldTab是否为null,那么可能会发生如下情况:

1.线程A创建newTab,还未将其给旧的table变量(HashMap的内部属性,是共享变量)赋值,之后时间片到期,切换线程B(线程A的空间中oldTab == null)

2.线程B看到table == null,也会创建table,此时线程B的空间中oldTab == null

3.线程B完成一系列插入操作后切换线程A

4.线程A执行,table = newTab 将刚创建的newTab赋值给table(这个table是HashMap的内部属性,是共享变量),此时线程B插入的数据将全部丢失

所以HashMap是线程不安全的,相比之下ConcurrentHashMap在确定要创建table时,进入到initTable()中仍旧会进行多次判断是否需要创建,并通过sizeCtl变量来最终判断是否需要创建table,而访问sizeCtl变量时线程安全的访问方式,所以ConcurrentHashMap在初始化table时是线程安全的。

4.2.2 插入元素

一共会碰到以下4种情况:

  1. 需要新建table[]
  2. table[i]处没有元素,无hash碰撞,直接插入
  3. table正在被其他线程扩容,帮助扩容
  4. 要在table[i]处插入元素
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;
            // 1.需要新建table
            if (tab == null || (n = tab.length) == 0)
                tab = initTable(); 
            // tabAt原子获取第i个位置的桶(链表头节点或者树的root)
            // 2.table[i]处没有元素,无hash碰撞
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                 // CAS设置第i个桶,如果此时其它线程已经在该位置插入元素,则会一直循环此CAS过程
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            // MOVED = -1, hash值为-1表示map正在扩容,就让本线程帮忙扩容加快速度
            // 这也是为什么hash值在计算时最后要与0x07777777按位与,保证为正数
           	// 这里解释下,hash值一共有以下几种可能:
            // -1: 表示整个map正在扩容
            // -2: 表示该结点为树的根结点
            // 3.table正在被其他线程扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            // 4.要在table[i]处插入元素
            else {
                V oldVal = null;
                synchronized (f) { // f:table[i]处链表首结点,上锁避免数据覆盖
                    if (tabAt(tab, i) == f) { // 二次检查
                        // 1.table[i]是链表
                        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;
                                }
                            }
                        }
                        // 2.table[i]处是树
                        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); // 转化为红黑树时也会锁住root结点
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount); // 能够到这里说明插入成功无重复,addCount()内部完成扩容判断以及扩容工作
        return null;
    }

// 树化前的准备
private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                tryPresize(n << 1);
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
                synchronized (b) {
                    if (tabAt(tab, index) == b) {
                        TreeNode<K,V> hd = null, tl = null;
                        for (Node<K,V> e = b; e != null; e = e.next) {
                            TreeNode<K,V> p =
                                new TreeNode<K,V>(e.hash, e.key, e.val,
                                                  null, null);
                            if ((p.prev = tl) == null)
                                hd = p;
                            else
                                tl.next = p;
                            tl = p;
                        }
                        setTabAt(tab, index, new TreeBin<K,V>(hd)); // TreeBin构造方法里构造红黑树
                    }
                }
            }
        }
    }


ConcurrentHashMap的插入过程

  • 如果是链表,会对链表头节点上锁,避免HashMap中出现的尾部插入数据覆盖的情况

put( )时实际上只对某个桶的首元素加锁

img

  • 如果是,除了对table[i]加锁,ConcurrentHashMap还会用treeBin对象代替TreeNode,一起保证并发过程中的数据安全

    TreeBin如下所示,实际上就是用一个对象包裹住树的root,用一个TreeBin代表一棵树

    static final class TreeBin<K,V> extends Node<K,V> {
            TreeNode<K,V> root;
        	...
            ...
            ...    
    }  
    

    用TreeBin去替换TreeNode这样做是因为即使对table[i]加锁,也有可能发生如下错误:

在这里插入图片描述

插入元素前为图1,此时对f结点上锁,之后插入元素x3,树可能会调整到图2,此时f并不是root结点,对f的锁并没有锁住root结点,其他线程完全可以修改树;

用TreeBin去替换TreeNode之后再发生同样的插入调整动作后:

在这里插入图片描述

x3即使因为调整换成了树的新root也不影响对整棵树的上锁

因此总体来看,插入元素时无论是针对链表还是针对树的插入结点操作都是线程安全的

4.2.3 扩容

扩容会在以下两种情况下发生:

  1. 在将要对某个链表树化时,如果table.length < MIN_TREEIFY_CAPACITY(默认为64),则不会转化为树,而是会进行一次扩容
  2. 插入元素成功后,容器内部元素总个数 > sizeCtl时

原理一样,这里仅看插入元素之后的扩容

final V putVal(K key, V value, boolean onlyIfAbsent) {
        // 插入元素
    	...
        ...
        ...
        // 扩容
        addCount(1L, binCount);
        return null;
    }

// 统计table中元素的个数,并扩容
private final void addCount(long x, int check) { // x:1(插入元素后个数+1)  check:原来的链表元素个数
        CounterCell[] as; long b, s;
    	// if内是使用线程安全的且尽量减少竞争的方式对check进行累加x的操作(类似LongAdder类)
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                fullAddCount(x, uncontended); // 完成累加操作(参照LongAdder中的longAccumulate())
                return;
            }
            if (check <= 1) // 链表元素个数 <= 1肯定不会扩容直接返回
                return;
            // 获取table元素个数s
            s = sumCount();
        }
    	
        if (check >= 0) { // check是binCount(即链表在插入元素之前的个数)
            Node<K,V>[] tab, nt; int n, sc;
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null && 
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n);
                if (sc < 0) { // 说明有其它线程在扩容或者在创建table中
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || 
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0) // 其它线程正在创建table 则本线程返回
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) // sc+1表示扩容线程+1
                        transfer(tab, nt); // 其它线程正在扩容则帮忙扩容
                }
                // 需要扩容
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2)) // 将sc改为负数表示正在扩容
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }

ConcurrentHashMap在扩容时对线程安全的改进有以下几点:

  1. 取消size变量,使用原子累加的方式去增加获取容器内的元素个数
  2. 扩容时转移链表上锁

扩容过程核心代码在transfer()函数中:

扩容的算法思想是多线程协作扩容,首先确定一个步长stride ,每个线程一次性将连续个stride位置的桶转移到新table中,然后继续往前寻找(倒序从table尾部往前寻找),找到下一个stride位置的桶,如果找不到

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // 步长
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
            transferIndex = n;
        }
        int nextn = nextTab.length;
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        boolean advance = true; // 指示本线程是否需要往前寻找要转移的桶
        boolean finishing = false; // to ensure sweep before committing nextTab
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            // while内找到从i到bound(中间有bride个元素)位置,本线程本次循环将帮忙将这些数据转移
            while (advance) {
                int nextIndex, nextBound;
                if (--i >= bound || finishing)
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            // 已经没有需要转移的数据了
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) {
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1); // 重置扩容阈值
                    return;
                }
                // 本线程已经不需要做扩容工作
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) { // sc-1表示帮助扩容线程个数-1
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) // sc不为初始值,表示仍然有线程正在扩容
                        return;
                    // 进入到此表示本线程结束本次转移数据时,已经没有线程在扩容
                    // finishing = true下一轮循环将完成扩容工作将nextTable赋值给table
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                synchronized (f) { // 数据转移过程同HashMap
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        if (fh >= 0) {
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                        else if (f instanceof TreeBin) {
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> lo = null, loTail = null;
                            TreeNode<K,V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node<K,V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode<K,V> p = new TreeNode<K,V>
                                    (h, e.key, e.val, null, null);
                                if ((h & n) == 0) {
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                }
                                else {
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                (hc != 0) ? new TreeBin<K,V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new TreeBin<K,V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true; // 某次转移一个步长的数据结束,advanc置true继续寻找下stride个位置转移
                        }
                    }
                }
            }
        }
    }

参考

ConcurrentHashMap线程安全

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值