jdk7HashMap与ConcurrentHashMap源码分析

HashMap

核心流程

hashMap核心结构就是数组+链表的实现,在new HashMap()时,初始化默认threshold阈值为16,并设置默认的负载因子0.75f(与数组扩容有关)。

当有新的元素插入时,会根据计算出来的容量和负载因子重新计算阈值为12,并初始化HashMap的table属性(Entry数组)以及hashSeed属性;判断key是否为空,为空则将元素put到第0个数组中;通过位移运算得到一个hash值以及key的数组下标;遍历整个链表如果有相同的key,则覆盖并返回旧值,否则modCount+1;判断map中size大于threshold而且当前数组位置中不等于null,则进行扩容操作,最后使用头插法向该链表中插入新元素,并size++。

当获取一个元素时,如果key为null,则循环数组下标为0的链表,返回key为null的值,否则计算key的hash值,遍历链表,判断key值是否相等,如果命中则返回该Entry,否则返回null;

当修改某个HashMap对象时,modCount会加1

put()方法核心点

(1)hash容量

该方法会在put()方法初始化容量的时候调用,通过测试不难发现,他是取得某个int变量值的以内的最大2次方的数。而实际上是通过Integer.highestOneBit((number - 1) << 1)操作获得不小于该int值的最小2次方数。

    private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }

(2)计算hash值

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }
    h ^= k.hashCode();
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

//与操作计算数组下标
static int indexFor(int h, int length) {
    return h & (length-1);
}

hash()方法首先有个hashSeed的判断,暂时不管;根据hashCode()方法获得hash值,然后连续通过右移异或操作让高位参与hash计算,使得散列更加均匀。通过indexFor()方法的与操作计算数组下标,其中length为数组长度,因为length的长度是2的次方数,减1操作是确保低位都为1,从而与操作取得数组下标。这就是为什么hash容量是2的次方数的原因。

(3)扩容和新增元素

先扩容在新增元素。扩容条件:HashMap的size大小大于等于阈值threshold(容量*0.75)。创建一个Entry数组,容量是原有的hash容量的2倍,遍历原有的table以及链表,将所有的Entry对象,重新计算hash值和数组下标(如果rehash为false的情况下,数组下标最终只有两种可能,要么原来的下标,要么是两倍原来的下标),put到新的map中,将新创建的newTable赋值到HashMap对象中的table属性中,并重新计算阈值。计算新增元素的hash值和数组下标元素,put到map中。

(4)多线程扩容导致循环链表

假设两个线程都创建了新的Entry数组并同时执行到 Entry<K,V> next = e.next; 其中一个线程(t2)假死,另一个线程(t1)执行完两个循环后(或完成了扩容),t2又开始执行了,此时t2的某个Entry中链表会出现循环链表。最终HashMap的table属性被t1创建的Entry数组覆盖,当调用get()或者put()方法时就有可能出现死循环。

    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, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
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;
            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;
        }
    }
}

具体过程如下:

  • t1先完成扩容

  • t2在执行到 Entry<K,V> next = e.next;时让出时间片后等到t1完成扩容后,再开始扩容,先处理第一个节点

  • 处理第二个节点。

  • 由于b指向a,所以接下来继续处理a,由于next=null循环就结束了

  • 如果t2创建的table数组覆盖了t1的,由于t1已经完成了扩容,所以c是指向b的,最后就变成了这种死循环链表

ConcurrentHashMap

大家都知道ConcurrentHashMap是使用分段锁来保证线程安全的,那么它是如何实现线程安全的呢?本质上通过UNSAFE的cas保证把一个Segment对象放入Segment数组中,Segment类实现了ReentrantLock类,当调用Segment#put()会通过ReentrantLock来实现加锁保证线程安全

初始化

当new ConcurrentHashMap()时,在内存中会生成类似如下图的对象,其中Segment数组大小为16,并会初始化ConcurrentHashMap的第一个segment对象及其属性table(HashEntry数组,大小为2),数组其他位置均为null。

 put()方法源码思路

  • 判断元素是否为null,为空则抛异常;
  • 对key进行hash得到hash值(这里的hash算法与HashMap有所不同),然后右移28位取高四位与15得到segment的下标值;通过UNSAFE.getObject()方法拿到当前数组位置的值,判断是否为null,为null则根据数组第一个Segment对象的table长度和负载因子创建HashEntry[]和Segment对象(这里就体现了初始化提前创建第一个segment对象的好处)最后通过while循环和unsafe的CAS操作将segment对象放入Segment数组中,并返回segment对象;
  • 最后调用segment对象的put()方法,这里会加锁保证线程安全(segment继承了ReentrantLock),先调用tryLock()方法尝试获取锁,如果获取到了,计算key的hash值和下标位置,定位具体的HashEntry并遍历这个链表,如果存在相同的key则创建一个HashEntry覆盖,如果不存在就用头插法将新创建的HashEntry对象插入链表中;
static final class Segment<K,V> extends ReentrantLock implements Serializable {
    ...
}
  • 期间会判断当前segment是否需要扩容,扩容条件:segment对象内hashEntry个数大于阈值而且hashEntry数组长度小于最大值(1 << 30),满足条件则扩容为原来的两倍;
  • 如果tryLock()方法没有获取到锁,调用scanAndLockForPut()方法,它会进行cpu层面的优化,因为如果使用lock()方法,会一直阻塞到获取到锁,阻塞的过程cpu资源就浪费了,此时用阻塞的时间调用scanAndLockForPut()去尝试创建一个HashEntry对象,但是不一定会创建成功,如果创建成功了并满足相关条件则调用lock()方法获取锁,并返回HashEntry对象(可能为null,但是不影响外层方法会判断是否需要创建HashEntry对象);modCount++

 get()方法

get方法获取的逻辑与HashMap类似,最重要的一点是这里会使用unsafe操作来获取数组的中segment对象保证原子性

总结

  1. HashMap的key和value可以为空。ConcurrentHashMap的key和value不能为空
  2. HashMap是线程不安全的,多线程执行可能导致死循环。ConcurrentHashMap是线程安全的,segment对象通过死循环+cas保证线程安全,插入链表是通过JDK的ReentrantLock保证线程安全
  3. HashMap扩容是扩容整个数组,大小为原来的两倍。ConcurrentHashMap是扩容某个segment下table数组,也是扩容到原来的两倍
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值