ConcurrentHashMap底层详解(JDK1.7)

为什么要使用ConcurrentHashMap

HashMap是线程不安全的,因为在put操作时可能会出现数据被覆盖的情况(JDK8),在JDK7中还存在扩容时产生死循环的问题。
。而使用线程安全的HashTable效率又非常低下,因此可以使用ConcurrentHashMap。

HashTable效率低下是因为使用synchronized来保证线程安全。当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞或轮询状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。

ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

JDK1.7中的ConcurrentHashMap

数据结构

ConcurrentHashMap是Segment数组 + HashEntry数组结构,HashEntry存储键值对。

一个ConcurrentHashMap包含一个Segment,Segment里包含一个HashEntry数组,每个HashEntry数组中是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁。每个Segment元素相当于一个小的HashMap。
在这里插入图片描述
ConcurrentHashMap类中包含一个Segment<K,V>类型的数组,名为segments。Segment类中包含一个HashEntry<K,V>类型的数组,名为table。HashEntry 类中包含键值对的内容以及下一个HashEntry的地址,HashEntry 就相当于链表的节点,节点中存储的内容是键值对。

一个ConcurrentHashMap中只有一个Segment<K,V>类型的segments数组,每个segment中只有一个HashEntry<K,V>类型的table数组,table数组中存放一个HashEntry节点。

对源码的分析先从小到大:HashEntry–> Segment–> ConcurrentHashMap。

HashEntry 类:

HashEntry 用来封装散列映射表中的键值对。在 HashEntry 类中,key,hash 和 next 域都被声明为 final 型,value 域被声明为 volatile 型。

static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;

        HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
        //只列出主要部分

HashEntry就是ConcurrentHashMap数据结构中最小的存储单元,它就是对应一个个的<k,v>节点,它的内部相对简单。

Segment 类:

关于Segment内部的实现相对HashEntry肯定是要复杂一点的,这里分两部分介绍,首先介绍它内部的成员变量。

成员变量
    //scanAndLockForPut中自旋循环获取锁的最大自旋次数
    static final int MAX_SCAN_RETRIES =
        Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;

    //存储着一个HashEntry类型的数组
    transient volatile HashEntry<K,V>[] table;

    /存放元素的个数,这里没有加volatile修饰,所以只能在加锁或者确保可见性(如Unsafe.getObjectVolatile)的情况下进行访问,不然无法保证数据的正确性
    transient int count;

    /存放segment元素修改次数记录,由于未进行volatile修饰,所以访问规则和count类似
    transient int modCount;

    /当table大小超过阈值时,对table进行扩容,值为(int)(capacity *loadFactor)
    transient int threshold;

    /负载因子
    final float loadFactor;

Segment的rehash扩容分析
/**
 * Doubles size of table and repacks entries, also adding the
 * given node to new table
 * 对数组进行扩容,由于扩容过程需要将老的链表中的节点适用到新数组中,所以为了优化效率,可以对已有链表进行遍历,
 * 对于老的oldTable中的每个HashEntry,从头结点开始遍历,找到第一个后续所有节点在新table中index保持不变的节点fv,
 * 假设这个节点新的index为newIndex,那么直接newTable[newIndex]=fv,即可以直接将这个节点以及它后续的链表中内容全部直接复用copy到newTable中
 * 这样最好的情况是所有oldTable中对应头结点后跟随的节点在newTable中的新的index均和头结点一致,那么就不需要创建新节点,直接复用即可。
 * 最坏情况当然就是所有节点的新的index全部发生了变化,那么就全部需要重新依据k,v创建新对象插入到newTable中。
*/
@SuppressWarnings("unchecked")
private void rehash(HashEntry<K,V> node) {
   
    HashEntry<K,V>[] oldTable = table;
    int oldCapacity = oldTable.length;
    int newCapacity = oldCapacity << 1;
    threshold = (int)(newCapacity * loadFactor);
    HashEntry<K,V>[] newTable =
        (HashEntry<K,V>[]) new HashEntry[newCapacity];
    int sizeMask = newCapacity - 1;
    for (int i = 0; i < oldCapacity ; i++) {
   
        HashEntry<K,V> e = oldTable[i];
        if (e != null) {
   
            HashEntry<K,V> next = e.next;
            int idx = e.hash & sizeMask;
            if (next == null)   //  Single node on list 只有单个节点
                newTable[idx] = e;
            else {
    // Reuse consecutive sequence at same slot
                HashEntry<K,V> lastRun = e;
                int lastIdx = idx;
                for (HashEntry<K,V> last = next;
                     last != null;
                     last = last.next) {
   
                    int k = last.hash & sizeMask;
                    if (k != lastIdx) {
   
                        lastI
  • 11
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

编程芝士

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值