史上最详细的HashMap详解--源码分析

史上最详细的HashMap详解–源码分析

ps.本文所有源码都是基于jdk1.6

数据结构(数组+链表)

《史上最详细的HashMap详解--源码分析》图1-1
如下代码所示,HashMap实际上是由Entry数组组成的,Entry就是一个单相链表。所以HashMap实际上就是一个数组和链表的结合体,如图1-1所示。

    transient Entry[] table;//HashMap实际上是由Entry数组组成的
    static class Entry<K,V> implements Map.Entry<K,V> {//这个Entry就是一个链表
           final K key;
           V value;
           Entry<K,V> next;
           final int hash;
           ...
    }

基础方法

put方法

public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);//key是null的情况单独处理
    int hash = hash(key.hashCode());//算出key的hash
    int i = indexFor(hash, table.length);//根据key的hash值计算出table数组的下标,下面有详细讲解
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {   //如果之前存在这个key,更新value
        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;
        }
    }

    modCount++;
    //不存在这个key,插入新value,此处会引发线程不安全,后面会说为什么
    addEntry(hash, key, value, i); 
    return null;
}

put方法是最常用的方法之一,了解put的实现,先得了解indexFor,addEntry方法,下面是它们的源码

void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    //由此可以看出链表的插入是插入在表头,原因是插入操作不需要遍历整个链表
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e); 
    if (size++ >= threshold)
        resize(2 * table.length); //容量不足要扩容
}
//根据hash值计算出table数组的下标
//length必须是2的幂次(比如16,这样一个数与上15都会小于等于15),通过与操作来提升性能
static int indexFor(int h, int length) {
    return h & (length-1);
}

remove方法

remove方法很简单,找到对应的元素,直接删除,没有什么特殊的

public V remove(Object key) {
    Entry<K,V> e = removeEntryForKey(key);
    return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
    int hash = (key == null) ? 0 : hash(key.hashCode());
    int i = indexFor(hash, table.length); //定位table下标,方式和put一样
    Entry<K,V> prev = table[i];
    Entry<K,V> e = prev;
    while (e != null) {
        Entry<K,V> next = e.next;
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k)))) {
            modCount++;
            size--;
            if (prev == e)
                table[i] = next;
            else
                prev.next = next;
            e.recordRemoval(this);
            return e;
        }
        prev = e;
        e = next;
    }
    return e;
}

扩容

先介绍几个概念
– capacity:容量,也就是前面说的Entry数组的大小,默认为16
– size:map中元素的个数
– loadFactor:装载因子,用来衡量map满的程度,默认为0.75f
– threshold:表示当size>=threshold的时候触发resize(扩容)操作,threshold = capacity * loadFactor
添加数据的时候发现空间不够用了(size>=threshold),就会进行resize,容量为原来的2倍
过程如下:
1.如果旧table的容量为最大容量,threshold = Integer.MAX_VALUE,直接return,也就是不扩容了
2.否则,申请一个新的Entry数组,容量为之前的2倍
3.循环遍历旧table中的元素,并且释放旧table中的资源,且为元素重新计算在新table中的下标,并插入到新table中
4.将table指向新table

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);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
}
void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {    //循环遍历原table
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;            //释放原table中的资源
            do {
                Entry<K,V> next = e.next;
                //为旧HashMap中的所有元素重新计算table下标
                int i = indexFor(e.hash, newCapacity);     
                e.next = newTable[i];
                //将重新计算完下标的元素插入到新的Table中
                newTable[i] = e;                   
                e = next;
            } while (e != null);
        }
    }
}

为什么说HashMap线程不安全

1、多线程put导致元素丢失
根据我们刚才的分析,put操作会调用addEntry函数

void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e); 
    if (size++ >= threshold)
        resize(2 * table.length);
}

现在假如A线程和B线程同时对table的同一个下标链表调用addEntry,两个线程会同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失

2、put一个非null的元素后,get出来的可能是null
我们知道put操作有可能会触发扩容,就是我们上面讲的resize,resize里面重新new一个Entry数组,其容量就是旧容量的2倍,这时候,需要重新根据hash方法将旧数组分布到新的数组中,也就是其中的transfer方法,在这个方法里,将旧数组赋值给src,遍历src,当src的元素非null时,就将src中的该元素置null,即将旧数组中的元素置null了,也就是这一句:

if (e != null) {
    src[j] = null;

此时若有get方法访问这个key,它取得的还是旧数组,当然就取不到其对应的value了。

    原文作者:yan_wenliang
    原文地址: https://blog.csdn.net/yan_wenliang/article/details/50976113
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞