HashMap和CocurrentHashMap源码介绍

2019-04-06  本文已影响0人  豆小豆33

先介绍HashMap

要了解hashmap首先需要了解哈希表。

关于哈希表,可以简单理解成是一个主干数组,每传入一个参数的时候,可以通过一个Key去获得想要的位置,从而获取对应的数组值。而通过key去获取数组的位置,就成了哈希表的关键。
一般我们都会通过hashCode方法将一个key转化成哈希值,再通过哈希值去获取数组下标。
我用一张图解释一下

1024555-20161113180447499-1953916974.png
介绍完哈希表,那么我们继续介绍一下hashmap的一些关键参数
    /**
     * 阈值,代表当前哈希表的主干数组最大有多少个,默认是16
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * The load factor used when none specified in constructor.
     * 负载因子,代表当前最多可以存储多少个value值
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

     /**
     * 哈希表的主干数组
     */
    transient Entry<K,V>[] table;

再介绍一下HashMap的一个重要内部类
static class Entry<K,V> implements Map.Entry<K,V> { final K key;
        V value;
        Entry<K,V> next;  //存储指向下一个Entry的引用,单链表结构 int hash;//对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算 

       /** * Creates new entry. */ 
       Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        } 

}

可以看出,其实这是一个
除了自身带有key 和value的对象外,还有下一个节点值。是不是很像链表的一个节点?

接着直接看put方法
    public V put(K key, V value) {
        //如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(24=16)
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
       //如果key为null,存储位置为table[0]或table[0]的冲突链上
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
        int i = indexFor(hash, table.length);//获取在table中的实际位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        //如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧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++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
        addEntry(hash, key, value, i);//新增一个entry
        return null;
    }    

简单总结一下就是
1.如果table为空,那么初始化table
2.将当前的key转化为hash值
3.通过哈希值获取在数组中的index
4.如果当前Key已存在,就进行覆盖。

再看一下get
public V get(Object key) {
     //如果key为null,则直接去table[0]处去检索即可。
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
 }

final Entry<K,V> getEntry(Object key) {
            
        if (size == 0) {
            return null;
        }
        //通过key的hashcode值计算hash值
        int hash = (key == null) ? 0 : hash(key);
        //indexFor (hash&length-1) 获取最终数组索引,然后遍历链表,通过equals方法比对找出对应记录
        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.通过key获取哈希值
2.通过哈希值获取当前数组下表index
3.获取数组下表以后就等于获取了当前链表的第一个节点
4.依次遍历链表找出最终对应Key的节点

哈希冲突

刚才我们看到,获取数组table的节点以后,会再获取当前节点的下一个节点,这是为什么呢?
因为这里就涉及到了一个节点冲突的问题。简单点将就是,不同的Key通过哈希转化以后,可能得到同一个index。如果出现了这种现象,当前的hash值通过拉链法去解决。
何为拉链法呢,就是其实table的每一个值都是一个节点,对应一条链表。当我们发现当前的Key所对应数组下标的value中,已经有值,我们就把新的节点挂在当前节点的后方,形成链表。
所以我们上面的get方法就可以看出,获取到tab的一个值之后,还需要循环当前链表。
用一张图来简单描述一下hashMap的数据结构


1024555-20161113235348670-746615111.png

从这里开始我们可以发现,hashMap的操作请求都没有做同步处理,所以HashMap也就不是线程安全。基于这种情况,jdk又推出了ConcurrentHashMap。也就是在HashMap的基础上增加了线程安全的逻辑。

ConcurrentHashMap

相比HashMap,ConcurrentHashMap是由Segment 的数组组成,这个数组的个数一共是16个,每一个Segment则是继承了RenteenLock。这样等于每一个Segment都是一个分段锁,可以实现同时16个线程的操作。而Segment里面的每一个值,就可以理解成是一个HashMap,只是增加了线程同步的HashMap。
先用一张图介绍一下:


3.png

然后我们再来看一下Segment的源码


接着就来分析一下put的操作

public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    // 1. 计算 key 的 hash 值
    int hash = hash(key);
    // 2. 根据 hash 值找到 Segment 数组中的位置 j
    //    hash 是 32 位,无符号右移 segmentShift(28) 位,剩下低 4 位,
    //    然后和 segmentMask(15) 做一次与操作,也就是说 j 是 hash 值的最后 4 位,也就是槽的数组下标
    int j = (hash >>> segmentShift) & segmentMask;
    // 刚刚说了,初始化的时候初始化了 segment[0],但是其他位置还是 null,
    // ensureSegment(j) 对 segment[j] 进行初始化
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    // 3. 插入新值到 槽 s 中
    return s.put(key, hash, value, false);
}

1.首先根据Key获取哈希值
2.获取哈希值之后通过特定的算法获取Segments数组对应的下标
3.通过下标获取当前key的节点所在的Segment
4.获取到Segment之后,其实就是Segment内部的操作了

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    // 在往该 segment 写入前,需要先获取该 segment 的独占锁
    //    先看主流程,后面还会具体介绍这部分内容
    HashEntry<K,V> node = tryLock() ? null :
        scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        // 这个是 segment 内部的数组
        HashEntry<K,V>[] tab = table;
        // 再利用 hash 值,求应该放置的数组下标
        int index = (tab.length - 1) & hash;
        // first 是数组该位置处的链表的表头
        HashEntry<K,V> first = entryAt(tab, index);
 
        // 下面这串 for 循环虽然很长,不过也很好理解,想想该位置没有任何元素和已经存在一个链表这两种情况
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {
                K k;
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        // 覆盖旧值
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                // 继续顺着链表走
                e = e.next;
            }
            else {
                // node 到底是不是 null,这个要看获取锁的过程,不过和这里都没有关系。
                // 如果不为 null,那就直接将它设置为链表表头;如果是null,初始化并设置为链表表头。
                if (node != null)
                    node.setNext(first);
                else
                    node = new HashEntry<K,V>(hash, key, value, first);
 
                int c = count + 1;
                // 如果超过了该 segment 的阈值,这个 segment 需要扩容
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node); // 扩容后面也会具体分析
                else
                    // 没有达到阈值,将 node 放到数组 tab 的 index 位置,
                    // 其实就是将新的节点设置成原链表的表头
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        // 解锁
        unlock();
    }
    return oldValue;
}

1.获取同步锁加锁
2.逻辑基本和HashMap的一致就不再详细说明了,上面注释已经很详细了
3.要注意在finally中会释放锁。

java 1.7和1.8的区别

由于会出现链表冲突导致一个数组的链表特别长的情况,1.8将通过链表去解决节点冲突的方式改成了红黑树。

上一篇 下一篇

猜你喜欢

热点阅读