ConcurrentHashMap分段加锁机制简析

ConcurrentHashMap是线程安全的HashMap,这个应该大家都知道,我就不多废话了,实现的思想是使用的分段加锁,使用分段加锁的作用当然就是可以有效提升并发量,可以对比一下所有操作都加锁的HashTable,就能明白分段加锁的好处了

既然说是分段加锁,那么我们可以猜想一下是根据什么依据来进行分段的呢?
我们都知道HashMap的底层是一个数组,当里面的键值对出现了hash冲突的话,就会挂载成为一个链表,链表的阈值达到了8之后就会转化为红黑树,既然底层的数据结构是数组的话,那么是否可以对数组来进行加锁呢?我们拿ConcurrentHashMap的put方法来分析一下其中的源码实现就知道了

// ConcurrentHashMap的put方法直接就是调用的类中的putVal方法,所以我这里就直接贴出来putVal方法了
final V putVal(K key, V value, boolean onlyIfAbsent) {
	// key和value都不允许为null,否则会报错
    if (key == null || value == null) throw new NullPointerException();
   	// 计算hash值
    int hash = spread(key.hashCode());
    // 当前链表中元素的个数
    int binCount = 0;
    // table就是底层的数组
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // 判断数组是否为空
        if (tab == null || (n = tab.length) == 0)
        	// 初始化数组,这里只有一个线程能进行初始化数组的操作
            tab = initTable();
        // 判断要put的元素的位置在数组中是为null,如果为null的话,说明当前位置没有值,不会发生hash冲突
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        	// 使用CAS操作将元素加入数组中,这样即使多个线程进入这个判断,也只有一个线程能put成功,失败的线程继续循环,保证了并发安全
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        // 扩容相关判断
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        // 如果进入了这个else判断的话,那么说明发生了hash冲突,需要使用链表或者红黑树来处理
        else {
            V oldVal = null;
            // 这里锁住的f在上面的代码中赋过值了,就是这句f = tabAt(tab, i = (n - 1) & hash)
            // 所以这里锁住的是发生hash冲突的一个Node节点,只是数组中的一个元素而已,而不是锁住了整个数组
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 如果key值一样的话,就使用新的value覆盖旧的value
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            // 如果key不同的话,e一开始其实就是f,将新的元素使用链表的形式挂载在之前的元素后
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    // 转化为红黑树之后,下次再有hash冲突的话,就直接将元素加入到红黑树中
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
            	// 如果binCount的值大于等于8的话,就将链表转化为红黑树
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

通过ConcurrentHashMap的put方法,我们可以发现,加锁分为了两种:

1、没有发生hash冲突的时候,如果添加的元素的位置在数组中是空的话,那么就使用CAS的方式来加入元素,这里加锁的粒度是数组中的元素

2、如果出现了hash冲突,添加的元素的位置在数组中已经有了值,那么又存在三种情况
(1)key相同,那么直接用新的元素覆盖旧的元素
(2)如果数组中的元素是链表的形式,那么将新的元素挂载在链表尾部
(3)如果数组中的元素是红黑树的形式,那么将新的元素加入到红黑树

第二种情况使用的是synchronized加锁,锁住的对象就是数组中的元素,加锁的粒度和第一种情况相同

得出结论,ConcurrentHashMap的分段加锁机制,其实锁住的就是数组中的元素,当操作数组中不同的元素时,是不会产生竞争的

上面所说的都是JDK1.8的ConcurrentHashMap的实现,那么这个实现机制和JDK1.7中的ConcurrentHashMap又有什么不同呢?

在JDK1.7中,ConcurrentHashMap使用的是segment锁,是继承自ReentrantLock,一旦初始化完成,就不能再改变了,但是segment数组中的HashEntry数组是可以改变的
jdk1.8中的ConcurrentHashMap中废弃了segment锁,直接使用了数组元素,数组中的每个元素都可以作为一个锁,在元素中没有值的情况下,可以直接通过CAS操作来设值,同时保证并发安全,如果元素里面已经存在值的话,那么就使用synchronized关键字对元素加锁,再进行之后的hash冲突处理
随着数组扩容的话,这里面的元素增多,可加的锁也增多了,所以说jdk1.8的ConcurrentHashMap加锁粒度比1.7更细,jdk1.7里的ConcurrentHashMap加锁只能对segment来加锁,而且初始化了就不能改变

综上所述,JDK1.8中的ConcurrentHashMap加锁的粒度更细,并发性能更好

你可能感兴趣的:(并发编程)