Java7、8中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没太读明白)

原文地址:http://blog.51cto.com/3265857/2312357

时间: 2024-08-01 07:36:49

Java7、8中HashMap和ConcurrentHashMap源码阅读的相关文章

Java7/8 中 HashMap 和 ConcurrentHashMap源码对比分析

网上关于 HashMap 和 ConcurrentHashMap 的文章确实不少,不过缺斤少两的文章比较多,所以才想自己也写一篇,把细节说清楚说透,尤其像 Java8 中的 ConcurrentHashMap,大部分文章都说不清楚.终归是希望能降低大家学习的成本,不希望大家到处找各种不是很靠谱的文章,看完一篇又一篇,可是还是模模糊糊. 阅读建议:四节基本上可以进行独立阅读,建议初学者可按照 Java7 HashMap -> Java7 ConcurrentHashMap -> Java8 Ha

HashMap和ConcurrentHashMap 源码关键点解析

第一部分:关键源码讲解 1.HashMap  是如何存储的? a.底层是一个数组 tab b. hash=hash(key) ,然后根据数组长度n和hash值,决定当前需要put的元素对应的数组下标, hash算法见红框. 2.数组长度是固定的,HashMap 可以无限put(k,v) ,为什么? HashMap  的元素个数大于threshold的时候,会进行resize() 扩容 3.如何实现扩容的? 扩容就是通过 resize() , 重新创建一个新数组,对所有元素rehash,放到新数组

ConcurrentHashMap源码阅读以及底层实现的简单分析

ConcurrentHashMap 是可以实现多线程并发的HashMap,它是线程安全的. 前面分析过 HashMap的源码,它和HashMap有很多的相同点一样,比如它也有 initialCapacity 以及负载因子 loadFactor 属性.而且他们的默认值也是16和0.75. static final int DEFAULT_INITIAL_CAPACITY =16; static final float DEFAULT_LOAD_FACTOR =0.75f; 和HashMap不同的是

ConcurrentHashMap 源码阅读小结

前言 每一次总结都意味着重新开始,同时也是为了更好的开始.ConcurrentHashMap 一直是我心中的痛.虽然不敢说完全读懂了,但也看了几个重要的方法,有不少我觉得比较重要的知识点. 然后呢,放一些楼主写的关于 ConcurrentHashMap 相关源码分析的文章链接: ConcurrentHashMap 扩容分析拾遗 并发编程--ConcurrentHashMap#addCount() 分析 并发编程--ConcurrentHashMap#transfer() 扩容逐行分析 并发编程-

HashMap TreeMap ConcurrentHashMap 源码

1 HashMap java se 1.6 1.1 父类 java.lang.Object 继承者 java.util.AbstractMap<K,V> 继承者 java.util.HashMap<K,V> 类型参数: K - 此映射所维护的键的类型 V - 所映射值的类型 所有已实现的接口: Serializable, Cloneable, Map<K,V> 直接已知子类: LinkedHashMap, PrinterStateReasons 1.2 类定义 publ

JDK1.8 ConcurrentHashMap源码阅读

1.  带着问题去阅读 为什么说ConcurrentHashMap是线程安全的?或者说 ConcurrentHashMap是如何防止并发的? 2.  字段和常量 首先,来看一下ConcurrentHashMap中的一些字段和常量,这些在接下来的操作中会用得到 2.1.  常量 从中,我们可以获得以下信息: 数组的默认容量是16,最大容量是1<<30 当添加元素的时候,将列表转成树的阈值是8.也就是说,相同位置上多个元素是以链表的形式存储的,而当链表的长度(元素的个数)超过8时,将其转为树 在对

Struts2源码阅读(一)_Struts2框架流程概述

1. Struts2架构图  当外部的httpservletrequest到来时 ,初始到了servlet容器(所以虽然Servlet和Action是解耦合的,但是Action依旧能够通过httpservletrequest取得请求参数), 然后通过Filter chain,Filter主要包括ActionContextCleanUp,它主要清理当前线程的ActionContext和 Dispatcher:FilterDispatcher主要通过AcionMapper来决定需要调用哪个Actio

Hadoop 副本放置策略的源码阅读和设置

本文通过MetaWeblog自动发布,原文及更新链接:https://extendswind.top/posts/technical/hadoop_block_placement_policy 大多数的叫法都是副本放置策略,实质上是HDFS对所有数据的位置放置策略,并非只是针对数据的副本.因此Hadoop的源码里有block replicator(configuration). BlockPlacementPolicy(具体逻辑源码)两种叫法. 主要用途:上传文件时决定文件在HDFS上存储的位置

ConcurrentHashMap 源码详细分析(JDK1.8)

ConcurrentHashMap 源码详细分析(JDK1.8) 1. 概述 <HashMap 源码详细分析(JDK1.8)>:https://segmentfault.com/a/1190000012926722 Java7 整个 ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全.所以很