ConcurrentHashMap笔记

    ConcurrentHashMap是支持多线程并发操做的哈希表,与HashTable类似,不支持nullkeyvalue,方法声明上也遵循了HashTable的规范。整体数据结构与HashMap相似,都是数组,按链地址法哈希具体的值。但ConcurrentHashMap内部按来组织,每一个段对应了一个或多个哈希entry。写操做(putremove等)都须要加排它锁,而读操做(get)不须要加锁,所以获取的值多是读操做的中间状态,尤为对(putAllclear),读操做可能只能获取部分值。迭代器和enumeration返回的是哈希表某个状态,不抛出ConcurrentModificationException。迭代器同一时刻只容许一个线程使用。 算法

ConcurrentHashMap的成员变量有: 数组

static final int DEFAULT_INITIAL_CAPACITY = 16; 数据结构

static final float DEFAULT_LOAD_FACTOR = 0.75f; 多线程

static final int DEFAULT_CONCURRENCY_LEVEL = 16; 并发

static final int MAXIMUM_CAPACITY = 1 << 30; app

static final int MIN_SEGMENT_TABLE_CAPACITY = 2; ssh

static final int MAX_SEGMENTS = 1 << 16; 函数

static final int RETRIES_BEFORE_LOCK = 2; this

 

其中大部分与HashMap一致。DEFAULT_CONCURRENCY_LEVEL必定程度上衡量了可支持的并发线程数;MIN_SEGMENT_TABLE_CAPACITY是每一个段最少的哈希entry,即每一个段中HashEntry数组最小容量;MAX_SEGMENTS是最大容许的段数目(源码中注释说是不小于1<<24,但默认的值给的是1<<16,而且对该值注释说”slightly conservative”;RETRIES_BEFORE_LOCKsizecontainsValue方法加锁时重试的最大次数,若是在屡次重试后仍没有获取锁,则线程进入中断状态在加锁队列中排队。 spa

简单来讲,ConcurrentHashMap是一个Segment<K,V>[]数组,每一个Segment<K,V>又包含一个HashEntry<K,V>[]数组(即维护了一个小的hash表),HashEntry数组每一个元素就是一个hash值对应的HashEntry链,具体的key-value对就放在这个链中。

先来讲说Segment<K,V>。这是一个内部类,派生自ReentrantLock。主要的成员变量包括:

static final int MAX_SCAN_RETRIES =

            Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;

transient volatile HashEntry<K,V>[] table;

transient int count;

transient int modCount;

transient int threshold;

final float loadFactor;

    其中,loadFactorthresholdHashMap中一致;table就是该段一个小的hash表;count是表中的元素个数;modCount是段中全部可变操做(putremoveclear等)的次数,在isEmptysize中用获得,若是值溢出可能会致使一些问题。

    段的主要操做是putremovereplaceclear。除了clear,每次操做都须要tryLock,若是未得到锁,则调用scanAndLockscanAndLockForPut,一方面继续请求得到独占锁,另外一方面检索表以找到待操做的节点(The main benefit is to absorb cache misses (which are very common for hash tables) while obtaining locks so that traversal is faster once)。若是屡次(最大为MAX_SCAN_RETRIES)请求都没能得到锁,则interrupt线程,进入队列等待直到得到锁。具体细节与ReentrantLock的实现有关。相比而言,clear操做就显得粗暴一点,直接调用lock加锁。此外,put可能致使rehash,也是以两倍的大小增加。

    再来看看ConcurrentHashMap本身的成员。ConcurrentHashMap最关键的构造函数:

    public ConcurrentHashMap(int initialCapacity,

                             float loadFactor, int concurrencyLevel)

    首先找到一个不小于concurrentcyLevel的数,这个数即ssize必须是2的幂,且2^sshift=ssize,由此获得segmentShiftsegmentMask

        int sshift = 0;

        int ssize = 1;

        while (ssize < concurrencyLevel) {

            ++sshift;

            ssize <<= 1;

        }

        this.segmentShift = 32 - sshift;

        this.segmentMask = ssize - 1;

    再看capcap是每一个段中的表初始大小,cap也是2的幂,最小是2cap×sszie>=initialCapacity

    if (initialCapacity > MAXIMUM_CAPACITY)

            initialCapacity = MAXIMUM_CAPACITY;

        int c = initialCapacity / ssize;

        if (c * ssize < initialCapacity)

            ++c;

        int cap = MIN_SEGMENT_TABLE_CAPACITY;

        while (cap < c)

            cap <<= 1;

    最后,建立段数组。s0比如一个段模板,初始化表大小为caprehashthresholdcap×loadFactor,根据s0建立段数组。

    Segment<K,V> s0 =

            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),

                             (HashEntry<K,V>[])new HashEntry[cap]);

        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];

        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]

        this.segments = ss;

    所以,concurrentHashMapssize个段,即表示最多支持ssize个线程同时写。

    concurrentHashMap的方法中,不少都有一个recheck的过程。

isEmpty()方法,先检查每一个段的count,若是有不为0直接返回false,不然累加modCount获得sum;接着再遍历一遍段,若是count有不为0则返回false,不然用以前的sum递减modCount,若最后sum不为0则返回false,不然为true。这样作是针对一个段进行检查的同时另外一个段正在进行修改的状况,意义和做用我还不是特别能理解。isEmpty并不须要加锁。

Size()操做都须要对每一个段进行加锁(也不必定,若是发现为空则直接返回0);

containsValuesize同样,在指定次数(RETRIES_BEFORE_LOCK)内没发现想要的值,则强制加锁进行排它性查找,若是找到则返回true;还有个contains,是为了保持与HashTable一致,实际调用的就是containsValue

getcontainsKey都不须要加锁,表面含义和代码都很容易理解,若是参数为null则抛出NullPointerException

put操做首先找到段数组中指定映射的段,没有则生成一个新的段,接着调用段的put方法插入数据,每次在链头部插入。

putIfAbsentput差很少,不一样的是只有在不存在指定key时才会插入新的key-value值,若是已经有了这个key则不执行更新。

putAll调用put进行插入;

remove调用segmentremove

replace调用segmentreplace

clear依次调用每一个段的clearclear并不会同时对全部段加锁,所以可能在执行clear操做时,有读操做发生会出现读取到clear操做的中间结果,putAll也有这种状况。

keySet返回一个keyset,该set有个弱一致性的迭代器,不抛出ConcurrentModificationException。迭代器的操做调用的是该concurrentHashMap相关的方法,包括removesizecontainsclearisEmpty等,不包括任何put操做。全部iterator上的改动都会直接反应到ConcurrentHashMap,反之亦然。

valuesentrySetkeySetelementskeys返回的是一个枚举集。

再说hash算法,HashMaphash算法已经够变态了,没想到一山还有一山高,ConcurrentHashMap也是特么的吓唬人啊。

// 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);

最后,ConcurrentHashMap的实现比起HashMap复杂不少,还有不少概念我理解的还很不到位,例如volatile的用法,happen-before究竟是什么意思?不加锁的读会出现什么可能意想不到的后果?等等。

相关文章
相关标签/搜索