ConcurrentHashMap解析

来源:互联网 发布:python串口数据采集 编辑:程序博客网 时间:2024/05/15 07:14

同步容器类在执行每个操作期间都会持有一个锁,有时当持有锁花费时间较长,此时其它线程在这段时间内都不能访问该容器,从而导致性能下降。

ConcurrentHashMap与HashMap一样,也是一个基于散列的Map,但它使用了一种完全不同的加锁策略来提供更高的并发性和伸缩性。ConcurrentHashMap使用粒度更细的称为分段锁(Lock Striping)的加锁机制实现更大程度的共享。

分段锁将数据分成一个一个的Segment来存储,并为每个Segment关联一个锁,这样当一个线程访问某一Segment时,其它Segment也能被其它线程访问,这样就实现了任意数量的读取线程可以并发访问Map,执行读取操作的线程和执行写入操作的线程可以并发地访问Map,并且一定数量的写入线程可以并发地修改Map。

ConcurrentHashMap内部结构图如下:


可以看到,ConcurrentHashMap内部包含很多Segment(默认为16个),每个Segment都继承自ReentrantLock类。



基本操作


构造函数

ConcurrentHashMap共有5个构造函数,源码如下:

public ConcurrentHashMap(int initialCapacity,                             float loadFactor, int concurrencyLevel) {        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)            throw new IllegalArgumentException();        if (concurrencyLevel > MAX_SEGMENTS)            concurrencyLevel = MAX_SEGMENTS;        // Find power-of-two sizes best matching arguments        int sshift = 0;        int ssize = 1;        while (ssize < concurrencyLevel) {            ++sshift;            ssize <<= 1;        }        segmentShift = 32 - sshift;        segmentMask = ssize - 1;        this.segments = Segment.newArray(ssize);        if (initialCapacity > MAXIMUM_CAPACITY)            initialCapacity = MAXIMUM_CAPACITY;        int c = initialCapacity / ssize;        if (c * ssize < initialCapacity)            ++c;        int cap = 1;        while (cap < c)            cap <<= 1;        for (int i = 0; i < this.segments.length; ++i)            this.segments[i] = new Segment<K,V>(cap, loadFactor);}

传入的参数有initialCapacity,loadFactor以及concurrentLevel三个:

  • initialCapacity表示初始容量,即Entry的个数,默认为16。
  • loadFactor表示负载因子,当Map中的元素大于loadFactor与最大容量的乘积时就需要rehash扩容,默认为0.75f。
  • concurrentLevel表示并发级别,用来确定Segment的个数,Segment的个数是大于或等于ConcurrentLevel的第一个2的平方数;理想情况下,ConcurrentHashMap的并发访问线程数量能够到达concurrentLevel,因为至少有concurrentLevel个Segment。


put操作

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);}
final Segment<K,V> segmentFor(int hash) {        return segments[(hash >>> segmentShift) & segmentMask];}

size操作

size操作与put和get操作最大的区别在于,size操作需要遍历所有的Segment才能算出整个Map的大小,而put和get都只关心一个Segment。假设我们当前遍历的Segment为SA,那么在遍历SA过程中其他的Segment比如SB可能会被修改,于是这一次运算出来的size值可能并不是Map当前的真正大小。一个比较简单的办法就是计算Map大小的时候将所有的Segment都Lock住,不能更新(包含put,remove等等)数据,计算完之后再Unlock,但这样会影响程序的性能。在JDK源码采用了一个更高效的办法:先给3次机会,不lock所有的Segment,遍历所有Segment,累加各个Segment的大小得到整个Map的大小,如果某相邻的两次计算获取的所有Segment的更新的次数(每个Segment都有一个modCount变量,这个变量在Segment中的Entry被修改时会加一,通过这个值可以得到每个Segment的更新操作的次数)是一样的,说明计算过程中没有更新操作,则直接返回这个值。如果这三次不加锁的计算过程中Map的更新次数有变化,则之后的计算先对所有的Segment加锁,再遍历所有Segment计算Map大小,最后再解锁所有Segment。源代码如下:

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;}


举例来说:一个Map有4个Segment,标记为S1,S2,S3,S4,现在我们要获取Map的size。计算过程是这样的:第一次计算,不对S1,S2,S3,S4加锁,遍历所有的Segment,假设每个Segment的大小分别为1,2,3,4,更新操作次数分别为:2,2,3,1,则这次计算可以得到Map的总大小为1+2+3+4=10,总共更新操作次数为2+2+3+1=8;第二次计算,不对S1,S2,S3,S4加锁,遍历所有Segment,假设这次每个Segment的大小变成了2,2,3,4,更新次数分别为3,2,3,1,因为两次计算得到的Map更新次数不一致(第一次是8,第二次是9)则可以断定这段时间Map数据被更新,则此时应该再试一次;第三次计算,不对S1,S2,S3,S4加锁,遍历所有Segment,假设每个Segment的更新操作次数还是为3,2,3,1,则因为第二次计算和第三次计算得到的Map的更新操作的次数是一致的,就能说明第二次计算和第三次计算这段时间内Map数据没有被更新,此时可以直接返回第三次计算得到的Map的大小。最坏的情况:第三次计算得到的数据更新次数和第二次也不一样,则只能先对所有Segment加锁再计算最后解锁。
1 0
原创粉丝点击