深入ConcurrentHashMap实现原理与使用

HashMap虽然经常被我们使用,但是也存在两个比较明显的缺点: 线程不安全、并发编程时容易死循环; 而使用HashTable虽然能解决并发问题,但是由于使用了synchronized来实现锁机制,也使得在并发时读写效率较低。

基于以上几点的话,我们会考虑使用ConcurrentHashMap来替代。不得不说的是concurrentHashMap的设计是比较出色的,它将数据分成一段一段的存储,然后给每一段数据加一把锁,每段之间的锁互不影响,也就是说每个Segment间是不需要考虑并发问题的,对于同一个Segment才需要锁机制。看源码会发现初始化的ConcurrentLevel为16,默认支持的并发数就是16,优雅的实现并发操作。

ConcurrentHashMap结构简介

实际来看,ConcurrentHashMap就是使用Segment数组把HashMap安全的重新"包装"了一下,其主要由Segment数组构成,Segment继承了ReentrantLock(可重入锁)
static final class Segment<K, V> extends ReentrantLock {
    ……
   transient volatile HashEntry<K, V>[] table;
   ……
}
而Segment里面的结构又是HashEntry数组,HashEntry的结构为链表,HashEntry以键值对的形式存储数据,所以我们说ConcurrentHashMap的结构就是数组+链表
transient volatile HashEntry<K, V>[] table;


ConcurrentHashMap源码解析

ConcurrentHashMap的构造器

public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
        /*Max_SEGMENTS最大为 1 << 16,即最大并发数65536*/
    if (concurrencyLevel > MAX_SEGMENTS)   
        concurrencyLevel = MAX_SEGMENTS;
    // Find power-of-two sizes best matching arguments
        /*2的sshift次方=ssize*/    
    /*往下可以看到ssize为segment数组的大小,和concurrentLevel直接相关*/
    int sshift = 0;   
    int ssize = 1;    
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
        /*segmentShift(段偏移量)和segmentMask(段掩码)用于定位Segment*/
    this.segmentShift = 32 - sshift;   
    this.segmentMask = ssize - 1;
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
        ++c;
    /*往下看可以发现,cap代表HashEntry数组的大小*/
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    while (cap < c)
        cap <<= 1;
    /*创建并初始化第一个segment数组*/ 
    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;
}
initialCapacity、loadFactor、concurrencyLevel若开发者未指定的话则取默认值
static final int DEFAULT_INITIAL_CAPACITY = 16;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int DEFAULT_CONCURRENCY_LEVEL = 16;

put操作

public V put(K key, V value) {
    Segment<K,V> s;
     /*ConcurrentHashMap的key和value均不能为空*/  
    if (value == null)
        throw new NullPointerException();
     /*散列算法*/
    int hash = hash(key);
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    return s.put(key, hash, value, false);
}
put方法主要包含两步:定位Segment、调用segment的put方法插入元素
定位Segment,可以看到ConcurrentHashMap的put方法首先调用了Wang/Jenkins hash的变种算法对元素的hashCode进行了一次再散列
private int hash(Object k) {
    int h = hashSeed;

    if ((0 != h) && (k instanceof String)) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

    // 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);
}
再散列的目的是为了尽可能的减少散列冲突(因为只要低位一样,无论高位是什么数,散列值都一致),使元素能够比较均匀的分布在不同的Segment上,也就加快了ConcurrentHashMap容器的读、取效率。倘若这个散列函数太差劲,就会导致元素过于集中式的分布在了一个Segment上,那么ConcurrentHashMap的读取效率就没法保证了,分段锁也将失去意义。如上的散列算法定位到Segment。

接着就是调用Segment的put方法插入元素,由于Segment的put方法需要对共享变量进行写操作,所以put方法是需要加锁操作的,put插入元素又分可以细分为两个步骤,是否扩容以及定位HashEntry放置元素
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
     /*put方法要加锁*/
    HashEntry<K,V> node = tryLock() ? null :
        scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        HashEntry<K,V>[] tab = table;
        int index = (tab.length - 1) & hash;
          /*定位HashEntry,可以看到之前在ConcurrentHashMap的put方法中产生的hash值在定位HashEntry时也用到了*/
        HashEntry<K,V> first = entryAt(tab, index);
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {
                K k;
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                e = e.next;
            }
            else {
                if (node != null)
                    node.setNext(first);
                else
                    node = new HashEntry<K,V>(hash, key, value, first);
                int c = count + 1;
                  /*扩容*/
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node);
                else
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        unlock();
    }
    return oldValue;
}
插入元素前会判断Segment里的HashEntry数组长度是否超过容量(threshold),超过就对HashEntry数组进行扩容(rehash),这点和HashMap的扩容判断更为"先进"一些,HashMap是插入元素后才判断数组是否达到阈值的,达到了进行扩容,这样的结果是有可能扩容后没有新的元素继续插入(ps:扩容是比较耗资源的,无效扩容就比较尴尬了…)。

顺带再看下扩容方法
private void rehash(HashEntry<K,V> node) {
    HashEntry<K,V>[] oldTable = table;
    int oldCapacity = oldTable.length;
    /*扩容为原来HashEntry数组的两倍*/
    int newCapacity = oldCapacity << 1;
    threshold = (int)(newCapacity * loadFactor);
    HashEntry<K,V>[] newTable =
        (HashEntry<K,V>[]) new HashEntry[newCapacity];
    int sizeMask = newCapacity - 1;
    for (int i = 0; i < oldCapacity ; i++) {
        HashEntry<K,V> e = oldTable[i];
        if (e != null) {
            HashEntry<K,V> next = e.next;
            int idx = e.hash & sizeMask;
            if (next == null)   //  Single node on list
                newTable[idx] = e;
            else { // Reuse consecutive sequence at same slot
                HashEntry<K,V> lastRun = e;
                int lastIdx = idx;
                for (HashEntry<K,V> last = next;
                     last != null;
                     last = last.next) {
                    int k = last.hash & sizeMask;
                    if (k != lastIdx) {
                        lastIdx = k;
                        lastRun = last;
                    }
                }
                newTable[lastIdx] = lastRun;
                // Clone remaining nodes
                for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                    V v = p.value;
                    int h = p.hash;
                    int k = h & sizeMask;
                    HashEntry<K,V> n = newTable[k];
                    newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                }
            }
        }
    }
    int nodeIndex = node.hash & sizeMask; // add the new node
    node.setNext(newTable[nodeIndex]);
    newTable[nodeIndex] = node;
    table = newTable;
}
可以看到,HashEntry扩容为原来的两倍,扩容时不时针对整个ConcurrentHashMap,而是只对需要扩容的那个Segment进行扩容(所以高效)

get操作

public V get(Object key) {
    Segment<K,V> s; 
    HashEntry<K,V>[] tab;
    int h = hash(key);
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    /*先定位到Segment*/
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
        (tab = s.table) != null) {
        /*再便遍历segment中的HashEntry数组*/
        for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                 (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
             e != null; e = e.next) {
            K k;
            if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                return e.value;
        }
    }
    return null;
}
ConcurrentHashMap的get操作是非常高效的,原因在于get操作不需要加锁。get方法里面使用到的共享变量都是volatile类型的,这些个变量能够在线程间保持可见性,能被多个线程同时读但是又能保证只被单线程写,并且不会读取到过期值。这样的变量就是HashEntry
transient volatile HashEntry<K,V>[] table;  //Segment里的~
volatile HashEntry<K,V> next;  //HashEntry链表中的~
使用volatile变量修饰后不会读取到过期值,是由于在java内存模型中的happen-before原则决定的,volatile修饰字段的写入操作总是优先于读操作,即使多个线程同时修改volatile变量字段,get字段也总能保证获取到最新的值(volatile变量相对于锁的优势体现出来了)。

引申文章:浅谈HashMap实现原理

猜你喜欢

转载自blog.csdn.net/fanrenxiang/article/details/80435459