源码剖析来学习ConcurrentHashMap

为什么要分析ConcurrentHashMap(以下简称 CHM)呢?

HashMap-HashTable,对Java熟悉的应该知道,HashMap操作高效,在平常工作时会经常用到,但它是无锁的,没考虑多线程环境,所以对于多线程环境下不适用,操作不安全,容易造成数据混乱,为此JDK特意出了一个HashTable的工具类,HashTable采用synchronized关键字修饰方法,使得多线程环境下,对HashTable的访问及操作都是串行执行的,以此保证操作数据的安全性,这样虽然弥补了HashMap不支持多并发的缺点,但也产生了一些新的问题,比如所有方法都用synchronized修饰,这会造成未获得资源锁的线程挂起,增加线程上下文切换,消耗CPU,效率较低。

好啦,言归正传,接下来开始分析源码,来看看CHM到底是怎样高效并发的!

源码分析

1、Segment

这是CHM中的Segment定义,从中可看出Segment 继承了 j.u.c 包下的ReentrantLock,每个segment一个锁,且这个锁是可重入的,可重入意味着 它不用像 synchronized 那样,每次操作都竞争,在相同线程操作时可重入,减少了一部分线程切换。

static final class Segment<K,V> extends ReentrantLock implements Serializable {
    transient volatile HashEntry<K,V>[] table;
}

 

Segment的 put、remove 、replace 、clear 用到了锁,无get方法,为啥没有呢,原因很简单,看上面Segment的定义,发现table 数组数据是用 volatile 关键字修饰了的,而 volatile 的特性就是一个线程修改,其他线程立即可见,所以直接将get()方法写在了CHM中,这里不得不佩服源码开发者,这个设计十分巧妙,为什么呢?因为我们一般用 CHM 是用来缓存数据的,当对 CHM 的操作中查询操作较多时,这个设计的效果就体现了,volatile 无锁,在多线程访问时,不用加锁、释放锁,提高了访问的效率,同时还降低了 CPU 切换上下文次数带来的负面影响,大大提高了程序速度。咦,到了这里,大家是否已经发现, CHM 已经对 HashTable 做了一个很重要的优化: get 不加锁。

这是 CHM 的 get() 方法源码

public V get(Object key) {
        Segment<K,V> s; // manually integrate access methods to reduce overhead
        HashEntry<K,V>[] tab;
        int h = hash(key);
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
            (tab = s.table) != null) {
            for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                     (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
                 e != null; e = e.next) {
                K k;
                if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                    return e.value;
            }
        }
        return null;
}

再看下 HashTable 的 get() 方法:

public synchronized V get(Object key) {
        Entry tab[] = table;
        int hash = hash(key);
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return e.value;
            }
        }
        return null;
}

2、Unsafe、CAS、DCL特性

我们来看下 put() 方法:

public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
}

首先,判断 value 为 null 时直接抛出异常,为什么没有判断 key ? CHM 不是不支持 key 为 null 吗?

别急,我们慢慢看下,往下走是根据 key 来计算 hash 值,看下 hash() 源码:

private int hash(Object k) {
        int h = hashSeed;

        if ((0 != h) && (k instanceof String)) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        // Spread bits to regularize both segment and index locations,
        // using variant of single-word Wang/Jenkins hash.
        h += (h <<  15) ^ 0xffffcd7d;
        h ^= (h >>> 10);
        h += (h <<   3);
        h ^= (h >>>  6);
        h += (h <<   2) + (h << 14);
        return h ^ (h >>> 16);
}

这里对 k 有操作,如果 k 为 null ,那么这里将会由 JVM 直接抛出空指针异常,所以 CHM 是不支持 key 为 null 的。

好,解决了疑问,我们再接着上面的 put() 方法往下看:

int j = (hash >>> segmentShift) & segmentMask;

这里通过 key 的 hash 值,通过位操作,得到段 ID 值 j,这就是 CHM 分段,再往下看:

if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);

这里通过上面得到的段 ID 值 j,去得到 Segment 对象,如果 s 为 null ,就会通过 ensureSegment() 方法得到一个新的段,

内部方法如下:

private Segment<K,V> ensureSegment(int k) {
        final Segment<K,V>[] ss = this.segments;
        long u = (k << SSHIFT) + SBASE; // raw offset
        Segment<K,V> seg;
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
            Segment<K,V> proto = ss[0]; // use segment 0 as prototype
            int cap = proto.table.length;
            float lf = proto.loadFactor;
            int threshold = (int)(cap * lf);
            HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
            if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                == null) { // recheck
                Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
                while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                       == null) {
                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                        break;
                }
            }
        }
        return seg;
}

其中,可以看到,ss = this.segments 被 final 修饰了,这里保证不可变,seg 是通过 UNSAFE.getObjectVolatile 来获取的,也就是说这个 seg 对象的修改对其他线程是立即可见的,然后通过 recheck 结合 CAS 操作,保证 segment 创建的原子性。

这里和我想的不一样,按理原子性的创建应该要用锁,但源码就偏偏不用,其实这里就是 DSL 的概念,双重检测锁。

3、分段锁

还是接着上面 put() 方法的最后一行

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

这里是调用了 segment 的 put () 方法,再进去看下

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                ......
                // 省略put逻辑
            } finally {
                unlock();
            }
            return oldValue;
        }

这里省略 put 进去的逻辑,主要看下锁的使用,看这段代码,好像没有锁定的方法是吧,其实有个隐藏的地方,scanAndLockForPut() 方法,在这方法的内部是用了 lock 的,并且 scanAndLockForPut 方法内部 lock 后没释放,在外层 put() 方法 finally 块进行释放的。这就是分段锁的使用,它锁定的并不是 CHM 对象,而是 一个 Segment ,这样就减少了锁的竞争。

 

总结CHM 的特性

1、不支持 为null 的Key 和 Value,与HashTable 一样
2、对HashTable 的改进版本,使用对 Segment段加锁,用锁分离技术替换单个锁,提高并发效率
3、通过key的 hash 值,按 hash值分段(Segment)存储
4、弱一致性(weakly consistent),not fail-fast

关键词:锁分离技术(每个segment一个锁,对锁进行粒化),SegmentCASvolatile,final,Unsafe,DCL(双重检测锁)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值