乱弹java并发(二)-- ConcurrentHashMap

本文详细解析了ConcurrentHashMap的高性能实现原理,包括分离写锁、读操作无锁及删除操作的弱一致性策略。同时,阐述了如何通过全局锁保证size和containsValues等全局操作的数据一致性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ConcurrentHashMap是JUC中一个高性能的线程安全的hash表,key和value都不可为null,ConcurrentHashMap通过如下手段来来达到它的高性能:

1、分离写锁:普通的Hash表通过桶数组+冲突链表来实现,ConcurrentHashMap增加了一个段结构(Segment),通过段数组+桶数组+冲突链表三层结构来实现,在进行put操作的时候先通过hash码定位到对应的段,然后在段上加锁,这样的话如果多个线程的写操作在不同的段上执行,那么这些线程的写操作不会相互阻塞,通过这种把锁从全局锁细化到段锁的手段来增加put操作的并发性。

2、读操作(get)无锁,那么ConcurrentHashMap是如何在不加锁的情况下保证数据的一致性的呢?这块的实现是ConcurrentHashMap最精妙的地方,来看看它的代码:

ConcurrentHashMap$Segment

        V put(K key, int hash, V value, boolean onlyIfAbsent) {
            lock();
            try {
                int c = count;
                if (c++ > threshold) // ensure capacity
                    rehash();
                HashEntry<K,V>[] tab = table;
                int index = hash & (tab.length - 1);
                HashEntry<K,V> first = tab[index];
                HashEntry<K,V> e = first;
                while (e != null && (e.hash != hash || !key.equals(e.key)))
                    e = e.next;

                V oldValue;
                if (e != null) {
                    oldValue = e.value;
                    if (!onlyIfAbsent)
                        e.value = value;
                }
                else {
                    oldValue = null;
                    ++modCount;
                    tab[index] = new HashEntry<K,V>(key, hash, first, value);
                    count = c; // write-volatile
                }
                return oldValue;
            } finally {
                unlock();
            }
        }

        V get(Object key, int hash) {
            if (count != 0) { // read-volatile
                HashEntry<K,V> e = getFirst(hash);
                while (e != null) {
                    if (e.hash == hash && key.equals(e.key)) {
                        V v = e.value;
                        if (v != null)
                            return v;
                        return readValueUnderLock(e); // recheck
                    }
                    e = e.next;
                }
            }
            return null;
        }
在Segment中有个关键的变量count,它是一个volatile修饰的变量,它起着非常重要的作用,当put的key值在hash段中不存在(暂时先看着一个分支)时,put操作完成之后把当前的元素个数设置给了count变量,在get操作的最开始读取了count变量的值,在JMM规范中对volatile变量的描述:volatile变量的写happens-before对volatile变量的读,而hb又有传递性,所以volatile变量的写之前的操作hb读volatile变量之后的操作,所以只要线程的put操作在其它线程的get操作之前完成,那么put操作后的数据对get操作时可见的,这样就达到了数据的一致性,来看另一个分支,当key在段中已经存在时直接把value赋值给Entry的value字段,而Entry的value字段也是volatile字段,所以也能保证数据更新后的可见性。但是这种实现方式无法保证强一致性,只能保证弱一致性:

put 
 get
 get返回
put完成 
当出现上面的执行序列时,get无法返回最终put更新的数据,所以ConcurrentHashMap牺牲了强一致性来换取性能。

上面代码只是实现了put和get之间如何达到数据一致性,我们知道在hash表中删除操作也可以修改数据,那么remove操作和get之间如何实现一致性呢,来看看代码:


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

        HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
            this.key = key;
            this.hash = hash;
            this.next = next;
            this.value = value;
        }

	@SuppressWarnings("unchecked")
	static final <K,V> HashEntry<K,V>[] newArray(int i) {
	    return new HashEntry[i];
	}
    }

    V remove(Object key, int hash, Object value) {
            lock();
            try {
                int c = count - 1;
                HashEntry<K,V>[] tab = table;
                int index = hash & (tab.length - 1);
                HashEntry<K,V> first = tab[index];
                HashEntry<K,V> e = first;
                while (e != null && (e.hash != hash || !key.equals(e.key)))
                    e = e.next;

                V oldValue = null;
                if (e != null) {
                    V v = e.value;
                    if (value == null || value.equals(v)) {
                        oldValue = v;
                        // All entries following removed node can stay
                        // in list, but all preceding ones need to be
                        // cloned.
                        ++modCount;
                        HashEntry<K,V> newFirst = e.next;
                        for (HashEntry<K,V> p = first; p != e; p = p.next)
                            newFirst = new HashEntry<K,V>(p.key, p.hash,
                                                          newFirst, p.value);
                        tab[index] = newFirst;
                        count = c; // write-volatile
                    }
                }
                return oldValue;
            } finally {
                unlock();
            }
        }

从代码中可以看到,在HashEntry结构中,除了value字段其它字段都是不可变的,在删除时,找到待删除的节点,把该节点之前的所有节点都复制,最后一个节点的next指针指向删除节点的后一个节点。这种方式也只能保证弱一致性(事实证明强一致性和性能两者只能取其一)。

因为ConcurrentHashMap时分段结构,所以对于size和containValues这种全局操作需要扫描所有段,所以为了保证数据一致性必须要加全局锁,对于这两个方法ConcurrentHashMap也做了优化:在加锁之前先尝试几次,比如size方法,先在不加锁的情况下扫描所有段,统计所有段的元素个数,并且统计modCount,如果modCount不为0,那么再检查一次,统计所有段的count并且比较之前记录各个段的modCount,一段modCount发生了变化或者前后两次统计的所有段的元素数量不一致,说明在统计的过程中hash表中有某些的段的数据发生了变化,那么这时候给所有段加锁重新扫描一次,containsValue的原理类似。

    public int size() {
        final Segment<K,V>[] segments = this.segments;
        long sum = 0;
        long check = 0;
        int[] mc = new int[segments.length];
        // Try a few times to get accurate count. On failure due to
        // continuous async changes in table, resort to locking.
        for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) {
            check = 0;
            sum = 0;
            int mcsum = 0;
            for (int i = 0; i < segments.length; ++i) {
                sum += segments[i].count;
                mcsum += mc[i] = segments[i].modCount;
            }
            if (mcsum != 0) {
                for (int i = 0; i < segments.length; ++i) {
                    check += segments[i].count;
                    if (mc[i] != segments[i].modCount) {
                        check = -1; // force retry
                        break;
                    }
                }
            }
            if (check == sum)
                break;
        }
        if (check != sum) { // Resort to locking all segments
            sum = 0;
            for (int i = 0; i < segments.length; ++i)
                segments[i].lock();
            for (int i = 0; i < segments.length; ++i)
                sum += segments[i].count;
            for (int i = 0; i < segments.length; ++i)
                segments[i].unlock();
        }
        if (sum > Integer.MAX_VALUE)
            return Integer.MAX_VALUE;
        else
            return (int)sum;
    }

以上代码都是基于JDK6的实现,ConcurrentHashMap在JDK7中的实现有比较大的变化,这个放到另外的文章分析。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值