走进源码—— HashMap(JDK 1.7 & JDK 1.8)

我门都知道 HashMap 是线程不安全的,那它不安全在哪?或者说,多线程情况下,它会怎么的不安全法?关键原因出在哪?带着这些疑惑,走一走,瞧一瞧咯,走过路过不u...,诶,好像有点偏了嘿。 

除局部方法或绝对线程安全的情形外,优先推荐使用 ConcurrentHashMap。二者虽然性能相差无几,但后者解决了高并发下的线程安全问题。 HashMap 的死链问题扩容数据丢失问题是慎用 HashMap 的两个主要原因。 

《码出高效》一书中如是说道。 

HashMap 体系中提到的三个基本存储概念,如下表所示:

名称 说明
table 存储所有节点数据的数组
slot 哈希槽。即 table[i] 这个位置
bucket 哈希桶。table[i] 上所有元素形成的表或数的集合

 

走进源码—— HashMap(JDK 1.7 & JDK 1.8)_第1张图片

JDK 1.7:

源码分析:

  • put():
public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);

    // 对象的hashCode与hash种子进行位运算的结果
    int hash = hash(key);
    // 上面的hash与数组长度进行位与运算的结果,确定哈希槽的位置
    int i = indexFor(hash, table.length);
    for (Entry e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    // 更改次数+1
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}
  • hash():
final int hash(Object k) {
    // hash种子
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

不晓得你们在这里有没有疑惑,存储的key对象自身明明就有hashCode()方法返回int哈希值,它干哈不直接用,干哈还弄个方法进行二次hash?

先搞清楚一个问题,hashCode() 的返回值它如果比你的数组长度要大,那岂不是要给你来一个ArrayIndexOutOfBoundsException?那还存个毛线。

这个hash方法乍一看也是一脸懵, 位异或操作5次,位移操作4次,究其原因还是减少Hash冲突,加大哈希码低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性 & 均匀性,最终减少Hash冲突。说的再直白点就是,让这个hash的高位同样来参与位运算。

再来看看 indexFor()

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);
}

根据HashMap的数组长度,与上哈希值,作为存储的数组下标位置,从而 解决 “哈希码与数组大小范围不匹配” 的问题。 

啥?对这个说法不满意?飘了??? 

首先这个方法的作用是返回数据元素存储在 table 数组的一个角标值, 

&运算操作符:对应 bit 位都为 1 结果才为 1,

首先这个 length 是 2 的 n 次幂,最高位肯定为 1,如果不减 1,其他的bit位肯定为 0,你一个hash值和 length 进行 & 运算操作,结果只有 length 或者 0,那么这hash也就失去了效用。减 1 之后,最高位是 0,但是其低位都为 1 了,这下取值就落在 0 ~ length - 1 的区间,也就是合法数组角标了。

  • addEntry():
void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        
        resize(2 * table.length);
        // 这里重新hash的原因是:上边的resize()的时候调用initHashSeedAsNeeded(),
        // 如果它改变了hashSeed,那么就会导致之前的hash值就没用,所以就重新hash吧
        hash = (null != key) ? hash(key) : 0;
        
        // 这里重新算hash槽的原因是:数组长度变了,你key.hashCode()值参与位与"&" 运算的长度也了,导致hash槽(运算结果,就是table数组的角标)跟着变
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}
  • createEntry():
void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}
  • resize(),扩容:
void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

    Entry[] newTable = new Entry[newCapacity];
    // 将旧的Entry数组中的数据传递到扩容后的数组中
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    // 将table的引用指向新的数组
    // 在此之前旧table依旧可以进行添加数据,多线程下,这里就会存在问题
    table = newTable;
    // 重新计算阀值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
  • initHashSeedAsNeeded(),根据需要初始化哈希种子:
final boolean initHashSeedAsNeeded(int capacity) {
    // hash种子是否已经初始化过,
    boolean currentAltHashing = hashSeed != 0;
    boolean useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    
    // 使用位^运算,只能够满足其中条件之一,如果同时满足或者都不满足就直接返回false
    boolean switching = currentAltHashing ^ useAltHashing;
    if (switching) {
        hashSeed = useAltHashing
            ? sun.misc.Hashing.randomHashSeed(this)
            : 0;
    }
    // 只有当hash种子为0,并且容量大于可选的哈希阈值(不指定或者指定为 -1 都是 2^31 - 1),才会给一个随机哈希种子,不然hash种子就是0
    
    return switching;
}
  •  transfer():
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry e : table) {
        while(null != e) {
            Entry next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

数据迁移,将旧数组中的数据一个一个放到新数组中,通过下面的源码可以看到,它其实采用的也是头插法,而hash桶添加的元素的方式本就是头插法,所以在transfer之后,这个hash桶就倒过来了。

扩容数据丢失问题: 

《码出高效》一书中也有说道:

 transfer() 数据迁移方法在数组在非常大时,会非常消耗资源。当前线程迁移过程中,其他线程新增的元素有可能落在已经遍历过的hash槽上,在遍历完成之后,table数组引用指向了 newTable,这时新增元素就会丢失,被无情的垃圾回收。

如果 resize 完成,执行了 table = newTable,则后续的元素就可以在新表上进行插入操作。但是如果多个线程同时执行 resize,每个线程又都会 new Entry[newCapacity], 这是线程内的局部数组对象,线程之间是不可见的。迁移完成后,resize 的线程会赋值给 table 线程共享变量,从而覆盖其他线程的操作,因此在 “ 新表 ”中进行插入操作的对象会被无情地丢弃。总结一下,HashMap 在高并发场景中,新增对象丢失原因如下:

  (1) 并发赋值时被覆盖。
  (2) 已遍历区间新增元素会丢失。
  (3) “新表”被覆盖。
  (4) 迁移丢失。在迁移过程中,有并发时, next 被提前置成 null。 

死链问题:

这事还得说回到 transfer() 数据迁移,以下用示例来进行说明:

正常情况下的 table

走进源码—— HashMap(JDK 1.7 & JDK 1.8)_第2张图片

 

两个线程进行 put() 操作,同时执行到 transfer():

线程 A,执行到 transfer() 方法,直到 e 为 2 的时候,执行完 Entry next = e.next; 此时,e为2,next为4,然后就被挂起了

线程 B 开始执行,执行完 transfer() 方法,然后被挂起,此时这个 table[3] 元素变成了下边这样:

走进源码—— HashMap(JDK 1.7 & JDK 1.8)_第3张图片

线程 A 继续,

此时,e 还是 2,next 为 4,但是 e.next 已经不是 4 了,

i 通过 indexFor() 算出还是 3 ;

e.next = newTable[i];    => 2.next = null

newTable[i] = e;            => newTable[3] = 2

e = next;                        => e = 4;

table[3] 是这样的:

再次 while 循环:

e 是 4,

next = e.next;                 => next = 2

i = indexFor();               => i = 3

e.next = newTable[i];    => 4.next = 2

newTable[i] = e;            => newTable[3] = 4

e = next;                        => e = 2;

此时 table[3] 变成了这样:

走进源码—— HashMap(JDK 1.7 & JDK 1.8)_第4张图片

第三次 while 循环:

e 是 2,

next = e.next;                 => next = null

i = indexFor();               => i = 3

e.next = newTable[i];    => 2.next = 4

newTable[i] = e;            => newTable[3] = 2

e = next;                        => e = null;

循环结束,死链形成,resize() 方法结束后 table = newTable;

此时 table[3] 变成了这样:

只要遍历这个 hash桶,便会进入死循环。

对于死链的生成,需要先明确三点:
  (1)原先没有死链的同一个 slot 上节点遍历一定能够按顺序走完。因为 e 和 next 都是线程内的局部变量,是绝对不会互相干扰,所以 while 循环在此生成死链的过程中是会正常退出的。
  (2)table 数组是各线程都可以共享修改的对象。
  (3)put()、get() 和 transfer() 三种操作在运行到此拥有死链的 slot 上,CPU 使用率都会飙升。
 

两个线程 A 和 B,执行 transfer 方法,虽然 newTable 是局部变量,但是原先 table 中的 Entry 链表共享的。产生问题的根源Entry 的 next 被并发修改。这可能导致:
  (1)对象丢失。
  (2)两个对象互链。
  (3)对象自己互链。 


 JDK 1.8

JDK 1.8 的 HashMap中,存储的实现是数组 + 链表 + 红黑树,当一个hash桶达到阀值,hash桶将由链表改为红黑树

啥也不说了,都在代码里,干了! 

  • 红黑树节点:

走进源码—— HashMap(JDK 1.7 & JDK 1.8)_第5张图片

  • 链表的节点:

走进源码—— HashMap(JDK 1.7 & JDK 1.8)_第6张图片

  •  它们之间的关系:

走进源码—— HashMap(JDK 1.7 & JDK 1.8)_第7张图片

  • put():
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
  • hash():
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这里将 1.7 中的 hashseed 种子给去掉了,也没那么多的位运算了。 

  • putval():
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    // 这个 p,就是hash桶的第一个元素
    Node[] tab; Node p; int n, i;
    // 判断数组是否为null(将 table 赋值给 tab 变量),或者数组的长度是否为0(将数组的长度赋值给n)
    if ((tab = table) == null || (n = tab.length) == 0)
        // 对table进行初始化
        n = (tab = resize()).length;
    // 将数组长度减 1 与上 hash 得到数组元素的角标,并且将该元素赋值给 p,判断该元素是否为null
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 角标位置上没有存储元素,用hash,key,value创建一个常规节点存储在 table 的 i 位置上
        tab[i] = newNode(hash, key, value, null);
    // node数组的这个位置已经存储了元素了,将元素存储到桶中
    else {
        Node e; K k;
        // 判断存储的 key 是否和hash桶的头节点是同一个对象
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // 键相同直接将该节点赋值给e
            e = p;
        else if (p instanceof TreeNode)
            // 如果 p 是一个树节点,将至强转成 TreeNode 之后进行 put 到红黑树中
            e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 对这个hash桶(链表)进行遍历
            for (int binCount = 0; ; ++binCount) {
                // 将节点 p 的下一个元素赋值给变量 e,并判断其是否为null
                if ((e = p.next) == null) {
                    // p后边没有元素,创建一个常规的节点作为p的next
                    p.next = newNode(hash, key, value, null);
                    // 判断这个hash桶的大小是否达到树化的阀值(加入一个元素之后,这个 hash 桶的长度就 +1 了)
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 这里是判断这个插入的 key 是否和 hash 桶中头节点之后的元素的 key 是同一个对象
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                // 等同于 p = p.next;
                p = e;
            }
        }
        // 判断 e 是否为 null,为 null,说明这个 key 是存在与之对应的 value 了,
        if (e != null) { // existing mapping for key
            // 获取 key 对应的 value
            V oldValue = e.value;
            // key 对应的 value 为 null 或者指定要改变现有值(onlyIfAbsent 为false),则更新value
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 将节点移动到最后
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 修改次数 +1
    ++modCount;
    // map大小+1,然后判断是否大于阀值
    if (++size > threshold)
        // 进行扩容
        resize();
    // 来决定是否移除最早放入Map的对象
    afterNodeInsertion(evict);
    return null;
}
  • resize():
final Node[] resize() {
    Node[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    
    int newCap, newThr = 0;
    if (oldCap > 0) {
        // 如果旧的数组长度已经达到最大的数组长度,将阀值调整到 2^31 -1,直接返回旧的数组对象
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 说明能扩容,将旧的数组容量 *2 之后赋值给新的数组容量,然后判断是否小于最大容量,并且旧的容量要大于等于默认的初始化容量。  
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 旧的阀值 *2 之后给新的阀值
            newThr = oldThr << 1; // double threshold
    }
    // 旧的容量小于等于0,说明这个数组未进行过初始化,或者进行过重新初始化
    else if (oldThr > 0) // initial capacity was placed in threshold
        // 这个数组未初始化过,哪来的旧的阀值??
        // 当你new这个对象的时候指定了initialCapacity,或者指定了initialCapacity + loadFactor,将会调用tableSizeFor()来算出这个旧的阀值(这个阀值是大于等于1的)
        // 旧的阀值大于0,将旧的阀值设为新的容量
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        // 零初始阈值表示使用默认值(在你不指定初始化阀值的时候会进入这里,设置默认值)
        newCap = DEFAULT_INITIAL_CAPACITY;
        // 加载因子 * 默认的初始化容量
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 新的阀值为0的时候重新设置阀值
    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[] newTab = (Node[])new Node[newCap];
    // 直接将 table 指向扩容后的数组对象
    table = newTab;
    
    if (oldTab != null) {
        // 旧表不为null,开始遍历旧的table,进行数据的迁移
        for (int j = 0; j < oldCap; ++j) {
            Node e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    // 如果数组对应下标位置的元素是一个红黑树,则拆分红黑树放到newTable中
                    // 拆分后的红黑树元素小于树化阀值,则转化为链表
                    ((TreeNode)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node loHead = null, loTail = null;
                    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;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
  • split(),将树形结构缩小或者直接还原(切分)为链表结构:
//参数介绍
//tab 表示保存桶头结点的哈希表
//index 表示从哪个位置开始修剪
//bit 要修剪的位数(哈希值)
final void split(HashMap map, Node[] tab, int index, int bit) {
    TreeNode b = this;
    // Relink into lo and hi lists, preserving order
    TreeNode loHead = null, loTail = null;
    TreeNode hiHead = null, hiTail = null;
    int lc = 0, hc = 0;
    for (TreeNode e = b, next; e != null; e = next) {
        next = (TreeNode)e.next;
        e.next = null;
        //如果当前节点哈希值的最后一位等于要修剪的 bit 值
        if ((e.hash & bit) == 0) {
                //就把当前节点放到 lXXX 树中
            if ((e.prev = loTail) == null)
                loHead = e;
            else
                loTail.next = e;
            //然后 loTail 记录 e
            loTail = e;
            //记录 lXXX 树的节点数量
            ++lc;
        }
        else {  //如果当前节点哈希值最后一位不是要修剪的
                //就把当前节点放到 hXXX 树中
            if ((e.prev = hiTail) == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
            //记录 hXXX 树的节点数量
            ++hc;
        }
    }


    if (loHead != null) {
        //如果 lXXX 树的数量小于 6,就把 lXXX 树的枝枝叶叶都置为空,变成一个单节点
        //然后让这个桶中,要还原索引位置开始往后的结点都变成还原成链表的 lXXX 节点
        //这一段元素以后就是一个链表结构
        if (lc <= UNTREEIFY_THRESHOLD)
            tab[index] = loHead.untreeify(map);
        else {
        //否则让索引位置的结点指向 lXXX 树,这个树被修剪过,元素少了
            tab[index] = loHead;
            if (hiHead != null) // (else is already treeified)
                loHead.treeify(tab);
        }
    }
    if (hiHead != null) {
        //同理,让 指定位置 index + bit 之后的元素
        //指向 hXXX 还原成链表或者修剪过的树
        if (hc <= UNTREEIFY_THRESHOLD)
            tab[index + bit] = hiHead.untreeify(map);
        else {
            tab[index + bit] = hiHead;
            if (loHead != null)
                hiHead.treeify(tab);
        }
    }
}

 

参考资料:

《码出高效》 

https://juejin.im/entry/5a140ca4f265da43310d728d

https://blog.csdn.net/carson_ho/article/details/79373026

https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/HashMap.md

https://juejin.im/entry/5839ad0661ff4b007ec7cc7a

你可能感兴趣的:(源码分析,源码分析,HashMap)