J.U.C之ConcurrentHashMap分析

来源:互联网 发布:9.0电脑刺绣制版软件 编辑:程序博客网 时间:2024/06/04 18:05

ConcurrentHashMap分析

一、ConcurrentHashMap介绍

HashMap一样,ConcurrentHashMap也是一个基于散列的Map,但它使用了一种完全不同的加锁策略来提供更高的并发性和伸缩性。ConcurrentHashMap并不是将每个方法都在同一个锁上同步并使得每次只能有一个线程访问容器,而是使用一种粒度更细的加锁机制来实现更大程度的共享。这种机制称为分段锁(Lock Striping)。在这种机制中,任意数量的读取线程可以并发访问Map,执行读取操作的线程和执行写入操作的线程可以并发地访问Map,并且一定数量的写入线程可以并发地修改MapConcurrentHashMap带来的结果是,在并发环境下将实现更高的吞吐量,而在单线程环境中只损失非常小的性能。

二、优劣

对于sizeisEmpty,这些方法的语义被略微减弱了以反映容器的并发特性。由于size返回的结果在计算时可能已经过期了,它实际上只是一个估计值,因此允许size返回一个近似值而不是一个精确值。虽然这看上去有些令人不安,但事实上sizeisEmpty这样的方法在并发环境下的用处很小,因为它们的返回值总在不断变化。因此,这些操作的需求被弱化了,以换取对其他更重要操作的性能优化,包括getputcontainsKeyremove等。

HashTablesynchronizedMap相比,ConcurrentHashMap有着更多的优势以及更少的优势,因此在大多数情况下,用ConcureentHashMap来替代同步Map能进一步提高代码的可伸缩性。只有当应用程序需要加锁Map以独占访问时,才应该放弃使用ConcurrentHashMap.

三、原理介绍



如上图,左侧为HashTable的加锁过程,右侧为ConcurrentHashMap的加锁原理。可见HashTable是锁整个表,而ConcurrentHashMap则是把map分段,锁段。比如分成了16segment,原来一次只有一个线程可以访问map,现在相当于可以有16个线程分别访问不同的segment下的数据了,并发性能提升明显。

我们看一下ConcurrentHashMap的数据结构:




可见上图,ConcurrentHashMap比普通的Map就多了一层Segment,相当于多做了一层Hash,数据过来先哈希判断分配到哪个Segment下,然后再Hash,通过拉链法存放Entry。Segment是一个ReentrantLock的子类,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。每个Segment守护一个HashEntry[]数组里的数据,当对HashEntry修改时,必须首先获得对应的Segment锁。

ConcurrentHashMap类图:



四、源码分析

4.1 get操作

ConcurrentHashMap.Get()

    public V get(Object key) {

        int hash = hash(key.hashCode());

        return segmentFor(hash).get(key, hash); //先经过hash取到Segment

    }

Segment.get()

        V get(Object key, int hash) {

            if (count != 0) { // read-volatile ,注意,是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; //注意,value也是volatile

                        return readValueUnderLock(e); // recheck

                    }

                    e = e.next;

                }

            }

            return null;

        }

    static final class HashEntry<K,V> {

        final K key;

        final int hash;

        volatile V value; //注意HashEntry中的valuevolatile

        final HashEntry<K,V> next;

}

注释:get操作的高效在于,整个get过程都不需要加锁,我们看上边的源码,完全没有锁的痕迹。ConcurrentHashMap没有用锁的关键在于它用了volatile变量,通过volatile变量在线程间内存的可见性来保证读取的数据不会是过期的。

根据Java内存模型的happen before原则,,对volatile变量的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值。

Volatile变量规则(volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”指时间上的先后顺序。

4.2put操作


先仔细看下ConcurrentHashMap的数据结构,对put源码的理解就容得多了。


ConcurrentHashMap.put

    public V put(K key, V value) {

        if (value == null)

            throw new NullPointerException();

        int hash = hash(key.hashCode());

        return segmentFor(hash).put(key, hash, value, false); //同样先定位桶。

    }

ConcurrentHashMap.putIfAbsent:key没有对应的value时才赋值,防止多线程重复put

    public V putIfAbsent(K key, V value) {

        if (value == null)

            throw new NullPointerException();

        int hash = hash(key.hashCode());

        return segmentFor(hash).put(key, hash, value, true);

    }

Segment.put()

        V put(K key, int hash, V value, boolean onlyIfAbsent) {

            lock();//加锁

            try {

                int c = count;

                if (c++ > threshold) // ensure capacity,判断是否达到阀值,是否需要扩容,这个扩容比HashMap好,HashMap是先增加元素,再判断是否需要扩容;这里是先判断是否需要扩,如果需要再扩,然后加入元素。HashMap的形式有可能刚好插入一个元素达到阀值,但是插入后扩容后不插入了,这就是一次浪费的扩容。而且ConcurrentHashMap的扩容是针对Segment的,不是针对整个Map的。

                    rehash();

                HashEntry<K,V>[] tab = table;

                int index = hash & (tab.length - 1); //定位table的位置                    HashEntry<K,V> first = tab[index];

                HashEntry<K,V> e = first; 

                while (e != null && (e.hash != hash || !key.equals(e.key)))

                    e = e.next;  //遍历HashEntry链表,根据hashkey定位HashEntry中的位置,即寻找桶

                V oldValue;

                if (e != null) { //如果找到了HashEntry,即现存的HashEntryhashkey都有,则用新值替换旧值

                    oldValue = e.value;

                    if (!onlyIfAbsent) //如果onlyIfAbsent =false,才赋值,值得一提的是putIfAbsent方法,传入的是true,也就是当map中有key对应的value的情况下不会重新赋值,这条分支永远不会走,只会当e==null的时候重新put新的keyvalue

                        e.value = value;

                }

                else { //如果没有,直接在链表头添加一个新的HashEntry

                    oldValue = null;

                    ++modCount;

                    tab[index] = new HashEntry<K,V>(key, hash, first, value);

                    count = c; // write-volatile

                }

                return oldValue;

            } finally {

                unlock();

            }

        }

4.3 size操作:

    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. 计算两次,看总count有没有变化

        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; //计算segment的总count

                mcsum += mc[i] = segments[i].modCount;//计算期间的modCount

            }

            if (mcsum != 0) {

                for (int i = 0; i < segments.length; ++i) {

                    check += segments[i].count;

                    if (mc[i] != segments[i].modCount) { //通过modCount比较容器变化

                        check = -1; // force retry

                        break;

                    }

                }

            }

            if (check == sum)

                break;

        }

        if (check != sum) { // Resort to locking all segments如果容量有变,锁住所有的put,remove,clean方法,计算,返回

            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;

    }

如果我们要统计整个ConcurrentHashMap里元素的大小,就必须统计所有Segment里元素的大小后求和。Segment里的全局变量count是一个volatile变量,那么在多线程场景下,我们是不是直接把所有Segment的count相加就可以得到整个ConcurrentHashMap大小了呢?不是的,虽然相加时可以获取每个Segment的count的最新值,但是拿到之后可能累加前使用的count发生了变化,那么统计结果就不准了。所以最安全的做法,是在统计size的时候把所有Segment的put,remove和clean方法全部锁住,但是这种做法显然非常低效。 因为在累加count操作过程中,之前累加过的count发生变化的几率非常小,所以ConcurrentHashMap的做法是先尝试2次通过不锁住Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。

那么ConcurrentHashMap是如何判断在统计的时候容器是否发生了变化呢?使用modCount变量,在put , remove和clean方法里操作元素前都会将变量modCount进行加1,那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。

参考: 

http://www.infoq.com/cn/articles/ConcurrentHashMap?utm_source=infoq&utm_medium=related_content_link&utm_campaign=relatedContent_articles_clk                                

原创粉丝点击