Java78中HashMap和ConcurrentHashMap源码阅读

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java78中HashMap和ConcurrentHashMap源码阅读相关的知识,希望对你有一定的参考价值。

首先来看下HashMap的类继承结构:

public class HashMap extends AbstractMap<K,V> impement Map<K,V>,Coloneable,Serializable{

 }

可以看出HashMap实现了Map接口。其里面的方法都是非线程安全的,且不支持并发操作。
对于HashMap主要看的是get/put方法实现,其在jdk1.7,及1.8在解决哈希冲突的上有所不同。
一、Java7 HashMap
技术分享图片

从上面的结构图中,可以大致看出,HashMap由数组:transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;没个元素对应为一个单向链表,链表数据结构如下:
    static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash; 
        }

在HashMap中定义的成员变量:
capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
loadFactor:负载因子,默认为 0.75。
threshold:扩容的阈值,等于 capacity * loadFactor,当容量超过这个值时,数组将扩容。
transient int modCount; //HashMap修改次数,这个值用于和expectedModCount期望修改次数比较。

1、put方法解析:
public V put(K key, V value) {
//1.当插入第一个元素时,需要创建并初始化指定大小的数组
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}

            //2.如果 key 为 null,循环遍历table[0]上的链表,最终会将这个 entry 放到 table[0] 中
    if (key == null)
        return putForNullKey(value);
            //3.计算key的哈希值
    int hash = hash(key);
            //4、通过h & (length-1)即h%length求模找到键值对放在哪个位置。
    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))) {//hash值为整数,比较性能比equals高;另外短路运算,哈希值系统了就没必要在比较equals。
            V oldValue = e.value;//先将当前节点的键对应的值取出来。
            e.value = value; //替换为新值。
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++; //容器修改次数加1
    addEntry(hash, key, value, i); //在指定的位置上添加数据,若空间不够则动态扩充,当前容量乘以2,新建一个数组,长度为capacity*2;并将原来的数组拷贝过来,更新对应变量。
    return null;
}

    数组初始化:
        private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize
    int capacity = roundUpToPowerOf2(toSize); //指定数组容量,默认为16

    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    table = new Entry[capacity]; //改变数组的引用,指向新创建的数组
    initHashSeedAsNeeded(capacity);
}

    计算键值对的位置:
        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); //等同于求模:h%length
}

    添加节点到链表中
        void addEntry(int hash, K key, V value, int bucketIndex) {
            //假如map的元素个数大于等于阈值,并且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);
}
   //新建一个Entry对象,插入单向链表表头,并增加size(不论是否扩容这一步都要进行)
   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++;
}

    数组扩容:
        void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

            //新建一个容量扩充2倍的数组
    Entry[] newTable = new Entry[newCapacity];
            //调用transfer方法将旧数组中的键值对拷贝过来
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
            //旧数组原来的堆空间设置为引用切断,指向新数组
    table = newTable;
            //重新计算阈值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

    键值对移植:
    /**
 * Transfers all entries from current table to newTable.
 */
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;
                            //是否重新计算key的哈希
            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;
        }
    }
}

    以上就是保存键值对的主要代码,基本步骤:
    1)、计算key的哈希值;
    2)、根据哈希值计算数组元素的保存位置(h&(length-1)或h%length);
    3)、根据需要扩充数组大小;
    4)、将键值对插入到对应的链表头部或更新已有值;

    2、get方法解析
        public V get(Object key) {
            //如果key为空则直接,在存放元素时是直接存放到table[0],所以直接调用getForNullKey方法遍历对应链表即可。
    if (key == null)
        return getForNullKey();
    Entry<K,V> entry = getEntry(key);

    return null == entry ? null : entry.getValue();
}
    遍历table[0]位置的链表,返回对应key==null的值,若果返回null,则有两种情况,要么没有key==null的键值对,要么对应位置上的值为null。
private V getForNullKey() {
    if (size == 0) {
        return null;
    }
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null)
            return e.value;
    }
    return null;
}

    key值不为空,则调用返回对应的值:
        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) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}

    总结基本流程:
    1、计算键的哈希值;
    2、根据哈希值找到数组中对于的链表;
    3、遍历链表,查找对应key的值;
    4、在比较查找的过程中,先快速比较哈希值,hash相同则再继续通过equals比较;

二、java7 ConcurrentHashMap
在java7 下ConcurrentHashMap结构如下:
技术分享图片

    ConcurrentHashMap是并发版的HashMap,支持复杂的并发操作,通过降低锁的粒度和cas等实现了高并发,支持原子条件的更新操作,不会抛出ConcurrentModificationException,实现了弱一致性。
    ConCurrentHashMap是一个Segment数组,每个segment元素对应一个哈希表(结构类似于HashMap)

    未完待续....(ConcurrentHashMap没太读明白)

以上是关于Java78中HashMap和ConcurrentHashMap源码阅读的主要内容,如果未能解决你的问题,请参考以下文章

HashMap 和 ConcurrentHashMap,Java1.8版本

深入理解java:2.3.3. 并发编程concurrent包 之容器ConcurrentHashMap

Java处理java.util.ConcurrentModificationException异常

验证HashSet和HashMap不是线程安全

(Concurrent)HashMap的存储过程及原理。

20 concurrentHashMap 1.8