HashMap源码分析

源码:
默认初始大小,值为16,要求必须为2的幂
 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
 最大容量,必须不大于2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
默认加载因子,值为0.7513 
static final float DEFAULT_LOAD_FACTOR = 0.75f;
HashMap空数组
static final Entry<?,?>[] EMPTY_TABLE = {};
可选的默认哈希阈值
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;复制代码

注意:java

jdk1.7中HashMap默认采用数组+单链表方式存储元素,当元素出现哈希冲突时,会存储到该位置的单链表中;算法

jdk1.8除了数组和单链表外,当单链表中元素个数超过8个时,会进而转化为红黑树存储,巧妙地将遍历元素时时间复杂度从O(n)下降到了O(logn))。数组

构造函数

一、无参构造函数:

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

二、带参构造函数,指定初始容量和加载因子:

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;
    threshold = initialCapacity;//和jdk8不一样,初始阈值就是初始容量,并没作2次幂处理
    init();
 }复制代码

三、带参构造函数,指定Map集合:bash

public void putAll(Map<? extends K, ? extends V> m) { 
    int numKeysToBeAdded = m.size(); 
    if (numKeysToBeAdded == 0)
        return; 
    if (table == EMPTY_TABLE) {
        inflateTable((int) Math.max(numKeysToBeAdded * loadFactor, threshold)); 
    } 
    if (numKeysToBeAdded > threshold) {
        int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
        if (targetCapacity > MAXIMUM_CAPACITY)
            targetCapacity = MAXIMUM_CAPACITY;
            int newCapacity = table.length;
        while (newCapacity < targetCapacity)
            newCapacity <<= 1;
        if (newCapacity > table.length)
        resize(newCapacity);
    }
    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
        put(e.getKey(), e.getValue());
    }复制代码

说明:执行构造函数时,存储元素的数组并不会进行初始化,而是在第一次放入元素的时候,才会进行初始化操做。建立HashMap对象时,仅仅计算初始容量和新增阈值。并发

添加元素

一、源码:

public V put(K key, V value) { 
    if (table == EMPTY_TABLE) { 
       inflateTable(threshold);//初始化数组 
    }  
    if (key == null)//key为null,作key为null的添加 
       return putForNullKey(value); 
    int hash = hash(key);//计算键值的哈希 
    int i = indexFor(hash, table.length);//根据哈希值获取在数组中的索引位置 
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {//遍历索引位置的单链表,判断是否存在指定key
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//key已存在则更新value值
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(hash, key, value, i);//key不存在,则插入元素
    return null;
}
private V putForNullKey(V value) {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {//key为null已存在,更新value值
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);//不存在则新增,key为null的哈希值为035 return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {//插入位置存在元素,而且元素个数大于等于新增阈值
        resize(2 * table.length);//进行2倍扩容
        hash = (null != key) ? hash(key) : 0;//扩容中可能会调整哈希种子的值,因此从新计算哈希值
        bucketIndex = indexFor(hash, table.length);//从新计算在扩容后数组中的位置
    }
    createEntry(hash, key, value, bucketIndex);//添加元素
}
//计算对象哈希值
final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {//String采用单独的算法
        return sun.misc.Hashing.stringHash32((String) k);
    }
    h ^= k.hashCode();//利用哈希种子异或哈希值,为了进行优化,增长随机性
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);//这里的移位异或操做属于扰乱函数,都是为了增长哈希值的随机性,下降哈希冲突的几率
}
void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);//新增元素插入到数组索引位置,原来元素做为其后继节点,即采用头插入方法
    size++;
}复制代码

二、流程图:

图注:添加元素流程图
图注:添加元素流程图

三、示例:

图注:初始状态
图注:初始状态

图注:添加10
图注:添加10

图注:添加18
图注:添加18

图注:扩容
图注:扩容

图注:扩容后添加
图注:扩容后添加

初始化数组

一、源码:

根据指定的大小,初始化数组 
private void inflateTable(int toSize) { 
    // Find a power of 2 >= toSize 
    int capacity = roundUpToPowerOf2(toSize); 
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//根据容量和加载因子计算阈值,最大为2^30+1 
    table = new Entry[capacity];//建立指定容量大小的数组 
    initHashSeedAsNeeded(capacity); 
}
//获取大于指定值的最小2次幂,最大为2^30
private static int roundUpToPowerOf2(int number) {
    // assert number >= 0 : "number must be non-negative";
    return number >= MAXIMUM_CAPACITY
            ? MAXIMUM_CAPACITY
            : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}复制代码

二、说明:

哈希种子,是为了优化哈希函数,让其值更加随机,从而下降哈希冲突的几率。经过HashMap中私有静态类Holder,在JVM启动的时候,指定-Djdk.map.althashing.threshold=值,来设置可选的哈希阈值,从而在initHashSeedAsNeeded中决定是否须要调整哈希种子。dom

private static class Holder { 
    /** * Table capacity above which to switch to use alternative hashing. */ 
    static final int ALTERNATIVE_HASHING_THRESHOLD; 
    static { 
        String altThreshold = java.security.AccessController.doPrivileged(
            new sun.security.action.GetPropertyAction(
                "jdk.map.althashing.threshold"));//经过-Djdk.map.althashing.threshold=值指定可选哈希阈值
        int threshold;
        try {
            threshold = (null != altThreshold)
                    ? Integer.parseInt(altThreshold)
                    : ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;//默认为Integer.MAX_VALUE
            // disable alternative hashing if -1
            if (threshold == -1) {
                threshold = Integer.MAX_VALUE;
            }
            if (threshold < 0) {
                throw new IllegalArgumentException("value must be positive integer.");
            }
        } catch(IllegalArgumentException failed) {
            throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
        }
        ALTERNATIVE_HASHING_THRESHOLD = threshold;//指定可选的哈希阈值,在initHashSeedAsNeeded做为是否初始化哈希种子的断定条件
    }
}
//根据容量决定是否须要初始化哈希种子
final boolean initHashSeedAsNeeded(int capacity) {
    boolean currentAltHashing = hashSeed != 0;//哈希种子默认为0
    boolean useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);//若是容量大于可选的哈希阈值,则须要初始化哈希种子
    boolean switching = currentAltHashing ^ useAltHashing;
    if (switching) {
        hashSeed = useAltHashing
            ? sun.misc.Hashing.randomHashSeed(this)//生成一个随机的哈希种子
            : 0;
    }
    return switching;
}复制代码

扩容

一、源码:

//按照指定容量进行数组扩容 
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];
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//按照扩容后容量从新计算阈值
}
//将元素从新分配到新数组中
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {//遍历原数组
        while(null != e) {
            Entry<K,V> 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;
        }
    }
}复制代码

二、问题:

上述扩容代码,在并发状况下执行,就会出现常说的链表成环的问题,下面经过示例来分析:
2.一、初始状态:
函数

图注:初始状态
图注:初始状态

线程1插入18,线程2插入26。此时线程1发现size为6,进行扩容。线程2发现size为6,也进行扩容。
2.二、 线程1执行:
线程1首先获取到CPU执行权,执行transfer()中代码:

for (Entry<K,V> e : table) { 
    while(null != e) { 
        Entry<K,V> next = e.next;//线程1执行到此行代码,e为10,next为2。此时CPU调度线程2执行。 
        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;
    }
}复制代码

2.三、 线程2执行:
线程2此时获取到CPU执行权,执行transfer()中代码:优化

for (Entry<K,V> e : table) { 
    while(null != e) { 
        Entry<K,V> 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;
    }
}复制代码

第一次遍历:e为10,next为2,rehash为false,i为2,newTable[2]为null,10.next为null,newTable[2]为10,e为2。
第二次遍历:e为2,next为null,rehash为false,i为2,newTable[2]为10,2.next为10,newTable[2]为2,e为null。
第三次遍历:e为null,退出循环。
注意,此时原table中元素2的next指向了10。
ui

图注:线程2执行扩容后结果
图注:线程2执行扩容后结果

2.四、 线程1执行:

for (Entry<K,V> e : table) { 
    while(null != e) { 
        Entry<K,V> next = e.next;//线程1执行到此行代码,e为10,next为2。CPU调度线程1继续执行。 
        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;
    }
}复制代码

当前:e为10,next为2,rehash为false,i为2,newTable[2]为null,修改:10.next为null,newTable[2]为10,e为2。
第二次遍历:当前:e为2,next为10【线程2执行后的结果】,rehash为false,i为2,newTable[2]为10,修改:2.next为10,newTable[2]为2,e为10。
第三次遍历:当前:e为10,next为null,rehash为false,i为2,newTable[2]为2,修改:10.next为2,newTable[2]为10,e为null,退出循环。
此时,链表成环,若是进行查找,会陷入死循环!!!
this

图注:线程1执行扩容后结果
图注:线程1执行扩容后结果

三、说明:

由上例可知,HashMap在jdk1.7中采用头插入法,在扩容时会改变链表中元素本来的顺序,以致于在并发场景下致使链表成环的问题。而在jdk1.8中采用尾插入法,在扩容时会保持链表元素本来的顺序,就不会出现链表成环的问题了。

总结

经过上述的分析,在这里总结下HashMap在1.7和1.8之间的变化:

  • 1.7采用数组+单链表,1.8在单链表超过必定长度后改为红黑树存储
  • 1.7扩容时须要从新计算哈希值和索引位置,1.8并不从新计算哈希值,巧妙地采用和扩容后容量进行&操做来计算新的索引位置。
  • 1.7插入元素到单链表中采用头插入法,1.8采用的是尾插入法。
相关文章
相关标签/搜索