Java HashMap原理

jdk7和jdk8的实现是不一样的,jdk7采用数组+链表实现,jdk8采用数组+链表+红黑树实现。

HashMap线程不安全,有线程安全需求的要用ConcurrentHashMap替代。

HashMap允许key为null,不允许key重复。

HashMap并发下put()会导致死链,导致cpu打满的原因不是死链的形成,而是查询时死链会导致无限循环。

jdk7版

主要成员变量

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认初始化大小 16 
static final float DEFAULT_LOAD_FACTOR = 0.75f;     // 负载因子0.75
static final Entry<?,?>[] EMPTY_TABLE = {};         // 初始化的默认数组
transient int size;     // HashMap中元素的数量
int threshold;          // 域值,每次扩容要重新计算下,用来判断是否需要调整HashMap的容量

Entry类型定义

// 在HashMap里的静态内部类 ,Entry用来存储键值对,HashMap中的Entry[]用来存储entry
static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;   //键
    V value;        //值
    Entry<K,V> next;  //采用链表存储HashCode相同的键值对,next指向下一个entry
    int hash;   //entry的hash值

    //构造方法, 负责初始化entry
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }

    public final K getKey() {
        return key;
    }

    public final V getValue() {
        return value;
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry e = (Map.Entry)o;
        Object k1 = getKey();
        Object k2 = e.getKey();
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            Object v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }

    public final int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }

    public final String toString() {
        return getKey() + "=" + getValue();
    }

    //当使用相同的key的value被覆盖时调用
    void recordAccess(HashMap<K,V> m) {
    }

    //每移除一个entry就被调用一次
    void recordRemoval(HashMap<K,V> m) {
    }
}

到这里就可以给出HashMap的抽象结构图了:

关于这种结构,我们还要了解的是存储对象是如何分布到数组和链表上的,数组是如何扩容的,搞完这两点,基本就没啥问题了。

以上这两个关键点,我们应该能从put()函数中得到解答

// 向map中添加key-value 键值对,如果可以包含了key的映射,则旧的value将被替换
public V put(K key, V value) {
    if (table == EMPTY_TABLE) {  // table如果为空,进行初始化操作
        inflateTable(threshold);
    }
    if (key == null)  // key 为null ,放入数组的0号索引位置
        return putForNullKey(value);
    int hash = hash(key);   // 计算key的hash值
    int i = indexFor(hash, table.length);  // 计算key在entry数组中存储的位置
    // 判断该位置是否已经有元素存在
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        // 判断key是否已经在map中存在,若存在用新的value替换掉旧的value,并返回旧的value
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);  // 空方法
            return oldValue;
        }
    }

    modCount++; // 修改次数加1 
    addEntry(hash, key, value, i); // 将key-value转化为Entry,添加到Map中,扩容操作在里面
    return null;
}

// 扩充表,HashMap初始化时是一个空数组,此方法创建一个新的Entry[]
private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize,power是幂的意思
    int capacity = roundUpToPowerOf2(toSize); // capacity为2的幂数,大于等于toSize
    // 计算阈值,用来确定需不需要扩容
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    table = new Entry[capacity];  // 新建数组,并重新赋值
    initHashSeedAsNeeded(capacity);  // 修改hashSeed 
}

// 根据hashcode,和表的长度,返回存放的索引,按位与的效果
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);
}

// 添加实体,bucketIndex是数组下标,关键点是扩容
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);
}

// 扩容操作,并发下可能形成死链,打满cpu
void resize(int newCapacity) {
    Entry[] oldTable = table;     // 将table赋值给新的引用
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    // 创建一个长度为newCapacity的数组
    Entry[] newTable = new Entry[newCapacity];  
    // 将table中的元素复制到newTable中
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    // 更改阈值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

// 将table中的数据复制到newTable中,死链形成
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    // 遍历原table
    for (Entry<K,V> e : table) {
        while(null != e) {
            // 遍历桶(链表)
            Entry<K,V> next = e.next; // 这个和下面的e = next配合形成一个循环
            if (rehash) { // 是否需要重新计算Hash值
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            // 重新确定所属数组角标
            int i = indexFor(e.hash, newCapacity); 
            e.next = newTable[i]; // 让被迁移的e指向头,首次为null
            newTable[i] = e; // 替换头,e是共享变量
            e = next; // 迭代下一个
        }
    }
}

迁移链表时并发下是如何产生死链的?如果多个线程同时进行扩容,肯定会形成多个新的数组+链表结构,但是迁移的数据还是原来的,在堆上并没有变化,只是被多个新的Hash结构引用了,假设线程1刚执行了next=e.next,就被挂起,线程2开始迁移并完成一个桶的迁移,然后线程1被唤醒,

 此时线程1看到的结构如下,其实已经被迁移了,但是它还是会操作

 

这里我们看到,经过3轮循环后,迁移结束了,但是环形链表形成了,下次查找时就会形成死循环,导致cpu某一个核心被打满,其中也能看到,并发resize()也可能会导致数据丢失,总之就是结果完全无法预知。

jdk8版本

jdk8中HashMap引入了红黑树,原因是碰撞频繁使,链表长度过长导致查询变慢,所以jdk8中,当链表的长度达到一定值(默认是8)时,将链表转换成红黑树(时间复杂度为O(lg n)),极大的提高了查询效率。

主要成员变量

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
transient Node<K,V>[] table;
transient int size;

可以看到,数组结构的类型已经变成了Node<>类型。Node<>结构定义如下:

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

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

看看jdk8中HashMap大致结构:

再来看看扩容是怎么做的:

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;
        }
        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
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    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) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> 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;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章