ConcurrentHashMap源码逐行解读(1.7)

本文深入解析了ConCurrentHashMap在JDK 1.7中的实现原理,包括构造函数、put方法等核心部分,并详细介绍了线程安全的实现机制。
ConCurrentHashMap源码解读(jdk1.7)

解读该源码最好先了解HashMap的源码

ConcurrentHashMap()

public ConcurrentHashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}

这个构造函数要看一下:

ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel)

public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)//判断参数是否合法
        throw new IllegalArgumentException();
    if (concurrencyLevel > MAX_SEGMENTS)//concurrentLevel  代表的是同时操作map的线程数
        concurrencyLevel = MAX_SEGMENTS;//MAX_SEGMENTS 多核cpu,这个值为64 单核 这个值为1
    // Find power-of-two sizes best matching arguments
    int sshift = 0;//记录ssize左移的次数
    int ssize = 1;//Segment数组的真正大小
    while (ssize < concurrencyLevel) {//将ssize改为不小于concurrencyLevel的2的幂次方
        ++sshift;
        ssize <<= 1;
    }
    this.segmentShift = 32 - sshift;
    this.segmentMask = ssize - 1;
    if (initialCapacity > MAXIMUM_CAPACITY)//不能超过最大容量 1<<30
        initialCapacity = MAXIMUM_CAPACITY;
    int c = initialCapacity / ssize;// 计算每个Segment中HashEntry数组的大小(注意这个c不是真正的数组大小)
    if (c * ssize < initialCapacity)//这一步是向上取整操作   例如 9/8=1   1*8 <9  1++ -> 2
        ++c;
    int cap = MIN_SEGMENT_TABLE_CAPACITY;//这个cap才是HashEntry数组的真正大小,最小是2
    while (cap < c)//找到一个2的幂次方  不小于c的 最小的数
        cap <<= 1;
    // create segments and segments[0]
    Segment<K,V> s0 =
        new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                         (HashEntry<K,V>[])new HashEntry[cap]);//定义Segment[0]
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];//定义Segment数组
    UNSAFE.putOrderedObject(ss, SBASE, s0); // 将s0 写入到 ss的第0个位置
    this.segments = ss;//将刚刚定义的Segment数组赋值给 segments
}

看完到这里,要明白一个事情:

ConcurrentHashMap<Integer, Integer> concurrentHashMap = new ConcurrentHashMap<>(9,0.75f,8);

使用这个构造函数,Segment数组大小是多少?

答案 8

每个Segment中,HashEntry数组大小是多少?

答案 2

这里要解释一下,为什么初始化只声明了一个s0,而没有将其他的也全部初始化好呢?

答案 因为没必要全部初始化好,其他的还有用到,用到的时候在创建Segment对象,可能有人要问了,s0这里其实也没有用到, 为什么要声明,这是因为 每次创建一个Segment对象要计算好几个值,初始化ConcurrentHashMap的时候初始化了一个s0,只有再要初始化的Segment对象的时候,就拿s0当模板直接照搬参数就行,这样就会快一点。

这个方法干什么的不用多解释了吧~

put(K key, V value)

public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)//这里提醒一下哦,HashMap是可以put(null,null)的,但是ConcurrentHashMap.put(null,null)是不可以的
        throw new NullPointerException();
    int hash = hash(key);//计算key的hash值
    int j = (hash >>> segmentShift) & segmentMask;//这行代码就是计算在Segment数组中的下标
    if ((s = (Segment<K,V>)UNSAFE.getObject          // 这一步是取出segments中下标为j的segment对象,赋值给s
         (segments, (j << SSHIFT) + SBASE)) == null) //  如果为空,则说说明这个地方还没放过内容
        s = ensureSegment(j);//声明一个segment对象  下面有讲解这个方法
    return s.put(key, hash, value, false);//真正的放入  下面有讲解这个方法
}

下面这个方法,干的事情,就几行代码:

Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);

UNSAFE.compareAndSwapObject(ss, u, null, seg = s)

return seg;

为什么写了看起来还挺长的挺多代码呢,这是因为要保证线程安全。

ensureSegment(int k)

private Segment<K,V> ensureSegment(int k) {
    final Segment<K,V>[] ss = this.segments;//首先获取segment数组
    long u = (k << SSHIFT) + SBASE; // u 其实就是偏移下标,这里可以理解为用k算出来的真正的下标:segment[index]
    Segment<K,V> seg;
    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {//将segment[u]取出,注意,这里是getObjectVolatile这个方法, 也就是说,别的线程要是修改了,segment[u],这个线程是可见的
        Segment<K,V> proto = ss[0]; // proto  取出ss[0],作为segment对象模板
        int cap = proto.table.length;//拿到Segment中HashEntry数组的长度
        float lf = proto.loadFactor;//拿到负载因子
        int threshold = (int)(cap * lf);//拿到阈值  ---- 这里是jdk1.7,没有红黑树,这个阈值是用于判断是否需要扩容重hash的
        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];//构建HashEntry数组
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))//二次检查是否segment[u]是否为null(即:在做上面那些操作时,看看是否有别的线程操作过segment[u])
            == null) {
            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);//去构建Segment对象
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))//再次检查segment[u]是否为null,注意,这里是while,之前的if,也是起到如果下面操作失败,再次检查的作用
                   == null) {
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))//CAS进行赋值,这里的比较并交换在CPU里面就是一条指令,保证原子性的,不存在  比较完,去交换的间隙 别的线程修改了这个值的可能
                    break;//设置完,跳出
            }
        }
    }
    return seg;//返回segment对象,注意,这里返回的seg可能是自己new的,也可能是别的线程new的
}

下面这个方法是ConcurrentHashMap真正开始去放入key-value,前面一直在做准备工作,segment对象初始化,保证拿到的segment对象肯定不是null。

前情:

ConcurrentHashMap和HashMap相比,最直白的最大的特点就是并发安全对吧,那么我们在加入元素的时候,先进行上锁,再进行放入元素,然后再解锁,是这样一个思路吧,好的,我们再看一下下面的代码,是不是就是这个思路呢。。

首先 tryLock() 然后进行了一些操作,最后你看finally里面是不是就是解锁。大致是这么个思路,接下来,开始逐行看。

对了,再提一个知识点:

我们知道 Segment 是继承了 ReentrantLock的,那么就可以调用 trylock() 和 lock()

这两个方法的区别在于:trylock是不阻塞的 lock是阻塞的

s.put(key, hash, value, false);

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    HashEntry<K,V> node = tryLock() ? null ://首先调用tryLock,尝试获取锁 获取到了就返回true
        scanAndLockForPut(key, hash, value);//获取锁失败就调用scanAndLockForPut方法  下面会讲解该方法
    V oldValue;
    try {//此时拿到锁的线程开始工作
        HashEntry<K,V>[] tab = table;//拿到 HashEntry数组
        int index = (tab.length - 1) & hash;//计算好下标
        HashEntry<K,V> first = entryAt(tab, index);//这个方法就简单理解为  从tab中拿到index位置的第一个元素
        for (HashEntry<K,V> e = first;;) {//e 记录刚刚拿到的第一个元素   ------  这里是个死循环
            if (e != null) {//判断是否被初始化过
                K k;
                if ((k = e.key) == key ||//判断key是否相等
                    (e.hash == hash && key.equals(k))) {//判断Hash,再判断equals,这个顺序也是有讲究的
                    oldValue = e.value;//找到了同样的key ,记录老value
                    if (!onlyIfAbsent) {
                        e.value = value;//更新value为我们的value
                        ++modCount;//修改次数 +1
                    }
                    break;
                }
                e = e.next;//更新e的位置
            }
            else {//此时说明 e到了链表尾部  或者 e没被初始化过
                if (node != null)//不为 null 说明之前做了准备工作
                    node.setNext(first);//头插法
                else//没进行准备工作
                    node = new HashEntry<K,V>(hash, key, value, first);//准备好HashEntry节点
                int c = count + 1;//记录 链表长度
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)//判断是否超过阈值
                    rehash(node);//扩容,进行 重复hash 这个方法就不看了,因为1.8超过阈值采用的是扩容和红黑树
                else
                    setEntryAt(tab, index, node);//将node放置在tab的index下标位置上
                ++modCount;//修改次数 +1
                count = c; //更新count值
                oldValue = null;// 此时oldValue应该为null
                break;
            }
        }
    } finally {
        unlock();//释放锁
    }
    return oldValue;//返回老value   如果没有key冲突,则是null
}

下面这个是在获取锁失败的时候调用,那么想想,获取锁失败,应该干嘛呢?这是ConcurrentHashMap为什么比HashTable效率高的原因所在了,HashTable想必大家应该知道,就是在HashMap的方法上用synconized关键字修饰,换成白话就是

HashTable获取锁失败,那么就进入阻塞状态,就什么都不干,一直等着

但是获取锁失败其实还可以去做别的事情,ConcurrentHashMap就是这样做的

ConcurrentHashMap获取锁失败,做了一些准备工作。初始化HashEntry,也就是将即将要加进去的元素构建成一个HashEntry对象

scanAndLockForPut(K key, int hash, V value)

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
    HashEntry<K,V> first = entryForHash(this, hash);//简单讲 拿到segment中HashEntry数组中通过hash计算的那个下标下的第一个节点
    HashEntry<K,V> e = first;//辅助变量   赋值一份当前取出来的 内容
    HashEntry<K,V> node = null;//辅助变量 
    int retries = -1; //  用于记录获取锁的次数
    while (!tryLock()) {// 获取锁,如果获取失败,就会去进行一些准备工作,  和HashTable等待的区别
        HashEntry<K,V> f; // 辅助变量用于重复检查   从segment中HashEntry数组中之前取出来的第一个节点是否还是我们之前取得那个
        if (retries < 0) {//准备工作,因为准备工作也不需要每次循环都去做对吧,最好的预期,准备工作,做一次就够了
            if (e == null) {//判断segment中的HashEntry是不是还没被初始化
                if (node == null) // speculatively create node
                    node = new HashEntry<K,V>(hash, key, value, null);//将我们即将插进去的元素,构建成HashEntry对象
                retries = 0;//将retries赋值为0,不让准备工作重复执行
            }
            else if (key.equals(e.key))//判断 key是否有重复的
                retries = 0;//如果有,也即找到了位置了,也不用再做准备工作了
            else
                e = e.next;//遍历segment中的HashEntry链表
        }
        else if (++retries > MAX_SCAN_RETRIES) {//判断最大的尝试获取锁的次数 同样,多核为64次  单核1次
            lock();//超过了最大次数,阻塞锁 ----  避免cpu空转
            break;
        }
        else if ((retries & 1) == 0 &&//偶数次数才进行后面的判断
                 (f = entryForHash(this, hash)) != first) {//重复检查segment第一个节点是不是被修改了
            e = first = f; // 此时为  别的线程修改了该segment的节点,重新赋值e first为最初值,和第一 第二行代码一样的效果
            retries = -1;//修改尝试标志   这样就会再去做准备工作
        }
    }
    return node;//将准备工作制作好的节点返回
}

注意看这个地方:

    else if ((retries & 1) == 0 &&//偶数次数才进行后面的判断
             (f = entryForHash(this, hash)) != first) {//重复检查segment第一个节点是不是被修改了

源码是如何判断segment中某个地方被动过的呢?

为什么 f = entryForHash(this, hash)) != first 这行的意思想必大家能看出来这样写的意思吧,因为这里是基于jdk1.7的,此时ConcurrentHashMap是头插法,所以只要比头节点即可。如果是1.8,那就不是比头节点了。

entryForHash(Segment<K,V> seg, int h)

static final <K,V> HashEntry<K,V> entryForHash(Segment<K,V> seg, int h) {
    HashEntry<K,V>[] tab;//赋值变量,用于记录当前 segment中的HashEntry数组
    return (seg == null || (tab = seg.table) == null) ? null ://判断当前HashEntry数组是否为null 如果是null 则直接返回null
        (HashEntry<K,V>) UNSAFE.getObjectVolatile
        (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);//通过hash计算下标  然后取出 tab中对应下标的第一个HashEntry节点
}

再次说明哦,这里是基于1.7的。
如有错误,欢迎指出哦。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值