HashMap源码解析

201 阅读5分钟

JDK7版本的源码解析

HashMap类是通过数组+链表的形式实现的

先来认识几个东东

类重要的属性

哈希表数组,长度必须为2的n次方
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

已经包含的元素个数
transient int size;

阈值,表示达到的元素个数需要扩容,threshold=capacity * load factor
int threshold;

负载因子
final float loadFactor;

哈希表中链表对象-重要的key-value对象

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;//链表的下一个对象
    int hash;

    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }
    
    public final int hashCode() {
        //两个对象分别取hash然后异或操作得到该key-value对象的hash值
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }

    public final String toString() {
        //如果打印的时候,是以key=value形式打印出来的
        return getKey() + "=" + getValue();
    }

    ...省略部分源码
}

最复杂的构造器

看一个最全的构造器,其它重载的构造器就不写了,无非是默认了方法参数,一看便知

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;//初始化阈值
    init();//空方法,忽略
}

public V put(K key, V value)

看了上面这个构造器,是不是很奇怪,为什么没有初始化table变量呢?其实是在put方法里面初始化了

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {//第一次来,那肯定是满足空的条件了
        inflateTable(threshold);//通过阈值参数去初始化table数组
    }
    if (key == null)//key为空的时候
        return putForNullKey(value);
    int hash = hash(key);//获取hashCode
    int i = indexFor(hash, table.length);//计算数组位置
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        //迭代指定的数组开始的链表
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            //e.hash通过addEntry源码发现就是key的hash值
            //key的哈希值一样,且key相等则说明链表中已经存在同样的key,需要将旧value更新为新value
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    //如果table[i]=null或者在链表中没有找到对应的key,那么就添加一个key-value对象到table[i]位置的链表处
    addEntry(hash, key, value, i);
    return null;
}

初始化table数组

private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize
    //传进来的是threshold,计算出最近的2的n次方的值
    int capacity = roundUpToPowerOf2(toSize);
    //threshold通过capacity * loadFactor计算出真正的值
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    //创建table数组,默认初始化为null
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
}

如何计算2的n次方的值的呢?

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;
            //借助Integer.highestOneBit求出整数对应二进制最高位为1的对应的十进制数
            //number为什么不直接左移一位,而是要减1在左移呢?可以用8(1000)和9(1001)手动算一下Integer.highestOneBit((number - 1) << 1)就明白了
            
}

添加key=null元素

当put的key=null的时候,处理方式有点不一样

private V putForNullKey(V value) {
    //找到数组第一个位置,并迭代从这个位置开始的链表,如果找到了key=null的,则修改值为新value,并返回旧value;如果数组第一个位置为null,则添加一个key=null,值为value的键值对,并返回null
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);//当table[0]==null的时候执行
    return null;
}

添加一条key-value到指定数组的位置

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
       //在元素个数大于等于阈值,并且指定的数组位置不为null的时候进行扩容
       //这个放到后面解析
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
    //在指定的数组位置添加一个key-value对象
    createEntry(hash, key, value, bucketIndex);
}
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++;
}

根据key的hashCode和数组的容量计算出数组位置

static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    //求余h%(length-1)的高级写法
    return h & (length-1);
}

数组扩容

现在重新看下addEntry方法的实现

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        //扩容为原先两倍大小,并安放集合中原先元素
        resize(2 * table.length);
        
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
    //在链表中添加一个元素, 见前面分析
    createEntry(hash, key, value, bucketIndex);
}

数组table扩容为原先2倍大小

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    //新建数组,newcapacity=2 * table.length,扩大为原数组大小的一倍
    Entry[] newTable = new Entry[newCapacity];
    //重点来了,执行扩容操作
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    //table指向扩容后的数组,即用新数组替换旧数组
    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) {//迭代每个数组指向的链表
            //当前k-v对象指向的下一个对象引用临时保存到next
            Entry<K,V> next = e.next;
            //忽略
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            //计算在扩容后新数组中位置
            int i = indexFor(e.hash, newCapacity);
            //当前k-v对象指向下一个k-v对象,newTable[i]如果为null,则指向null;否则指向k-v对象
            e.next = newTable[i];
            //赋值操作,这个时候e引用指向的是newTable[i]
            newTable[i] = e;
            //移动引用指针e到之前保存到临时变量的下一个节点
            e = next;
        }
    }
}

public V get(Object key)

接下来解析get方法,这个就相当简单了

public V get(Object key) {
    //key=null时候从table[0]查询
    if (key == null)
        return getForNullKey();
        
    //key!=null 查询    
    Entry<K,V> entry = getEntry(key);
    return null == entry ? null : entry.getValue();
}

查询key=null的value

private V getForNullKey() {
    if (size == 0) {
        return null;
    }
    //迭代table[0]位置的链表
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null)
            return e.value;
    }
    return null;
}

查询key!=null的value

final Entry<K,V> getEntry(Object key) {
    if (size == 0) {
        return null;
    }

    int hash = (key == null) ? 0 : hash(key);
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {//通过key哈希值定位到数组位置,并迭代该数组表示的链表
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            //找到了key直接返回
            return e;
    }
    return null;
}

线程安全问题

温习关键步骤

从上面分析可知,在put方法里面有个扩容的操作transfer。现在抽出几个关步骤:

for (Entry<K,V> e : table) {
    while(null != e) {
        Entry<K,V> next = e.next;
        int i = indexFor(e.hash, newCapacity);
        e.next = newTable[i];
        newTable[i] = e;
        e = next;
    }
}

分析一种导致线程安全情况

导致线程安全的情况不限于此。

假设现在有两个线程T1和T2,同时将table[i]的第一个链表元素重新哈希到新数组的位置(多线程操作共享资源形成了竞争关系)

分析1: T1已经执行到newTable[i] = e;那么此时n.next=null(因为刚开始newTable[i]是null);线程T1停下来了;

分析2: T2开始从Entry<K,V> next = e.next;执行,那么由于T1的执行,此时T2线程执行完Entry<K,V> next = e.next后,next=null,在执行e.next = newTable[i];则e.next指向了自己e,形成了循环;在执行e = next;的时候e==null,while将退出,并开始下一个for循环

从上面可知,在扩容后会出现循环链表的情况,那么在结合之前分析get方法,可知在get中for循环的条件一直满足,但是if条件一直不满足的情况下,则出现了查询死循环,get方法一直得不到返回,有可能导致CPU 100%

容量必须是2的n次方大小?

是的,是的

两点原因

  1. 让元素尽可能平均分布,减少哈希碰撞
  2. 利用二进制计算效率。在2的n次方的前提下,h%(length-1)与h & (length-1)会相等,而且按位与的效率更高