Java集合-ConcurrentHashMap工作原理和实现JDK7

概述

本文学习知识点

1.ConcurrentHashMap与HashMap的区别。
2.数据存储结构。
3.如何提高并发读写性能。
4.put和get方法源码实现分析。
5.size方法如何实现。

与HashMap的区别

1.ConcurrentHashMap和HashMap都是Map的实现,提供key,value的读写。
2.都继承自AbstractMap类,但实现的接口不同,如下图:

Java集合-ConcurrentHashMap工作原理和实现JDK7_第1张图片
ConcurrentHashMap与HashMap类图

3.ConcurrentHashMap是线程安全的,HashMap非线程安全。
4.两者采用的数据结构不同。ConcurrentHashMap采用segement+entry数组的方式,HashMap采用Entry数组的方式

数据结构

如下图描述了ConcurrentHashMap的存储结构,一目了然:

Java集合-ConcurrentHashMap工作原理和实现JDK7_第2张图片
ConcurrentHashMap数据存储结构

根据这个结构图,我们再看看源码中的声明:

public class ConcurrentHashMap extends AbstractMap
        implements ConcurrentMap, Serializable {
    // 默认segment个数
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    // 默认的更新map的并发级别,即默认最多允许16个线程进行并发写,  
    // 这个级别与segment的个数一致
    static final int DEFAULT_CONCURRENCY_LEVEL = 16;
    /**
     * The segments, each of which is a specialized hash table.
     */
    final Segment[] segments;
    // 其他省略
}

这里的segments数组中,每个segments其实是一个hash表,即HashEntry数组,如下,采用内部类实现方式:

static final class Segment extends ReentrantLock implements Serializable {
    // Hash table,默认初始容量为2,特别注意,这里table用了volatile修饰,
    // 保证多线程读写时的可见性
    transient volatile HashEntry[] table;
    // segment中hashtable的元素数量计数器,用于size方法中,分段计算汇总
    transient int count;
    // 执行更新操作时,获取segment锁的重试次数,多核CPU重试64次,单核CPU重试1次
    static final int MAX_SCAN_RETRIES =
            Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
    // 其他省略
}

Segment类,继承自ReentrantLock,因此每个Segment元素都是一个锁,锁的粒度从Hashtable类的整个表,降低到单个Segment元素,以支持分段锁,提供更高的并发读写能力,再来看下segments及其内部HashEntry数组怎么初始化,以及初始化容量大小,构造方法如下:

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;// segments数组的大小
        // 根据并发级别算出segments的大小以及2的指数
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
        this.segmentShift = 32 - sshift;
        this.segmentMask = ssize - 1;
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)
            ++c;
        int cap = MIN_SEGMENT_TABLE_CAPACITY;// segment内部HashEntry的默认容量,为2
        while (cap < c)
            cap <<= 1;
        // create segments and segments[0],即创建segments数组,
        // 并初始化segments[0]元素的HashEntry数组
        Segment s0 = new Segment(loadFactor, (int)(cap * loadFactor),
                             (HashEntry[])new HashEntry[cap]);
        Segment[] ss = (Segment[])new Segment[ssize];
        // UNSAFE为sun.misc.Unsafe对象,使用CAS操作,
        // 将segments[0]的元素替换为已经初始化的s0,保证原子性。
        // Unsafe类采用C++语言实现,底层实现CPU的CAS指令操作,保证原子性。
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }

从构造方法可以看出,ConcurrentHashMap初始化时,先根据并发级别数,计算出Segment数组的大小ssize和每个Segment中HashEntry数组的大小cap,并初始化Segment数组的第一个元素;Segment数组的大小为2的幂次方,默认为16,每个Segment内部HashEntry数组大小也是2的幂次方,最小值为2,也是默认的初始大小。

put方法实现

public V put(K key, V value) {
        Segment s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);
        // 根据hash值、segmentShift,segmentMask(段的长度)计算定位到段的索引下标
        int j = (hash >>> segmentShift) & segmentMask;
        // (j << SSHIFT) + SBASE)这段代码也是为了定位段下标。
        // 如果通过Unsafe类的CAS读取段下标元素,元素没有初始化,
        // 则调用ensureSegment进行初始化
        if ((s = (Segment)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
}

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

put过程:
1.通过key的hashCode值再计算hash值。
2.通过hash值找到segments数组的下标。
3.检查下标segment是否已经初始化,如果没有初始化,则调用ensureSegment进行初始化,内部用了CAS操作进行替换,达到初始化效果。初始化的过程进行了双重检查,UNSAFE.getObjectVolatile,通过这个方法执行了两次,以检查segment是否已经初始化,以及用UNSAFE.compareAndSwapObject进行CAS替换,CAS的替换有失败的可能,因此源码中还加了自旋重试的操作,保证最终CAS操作的成功。ensureSegment的实现源码就不贴出来了,读者自行看下。
4.再调用segment类的put方法,将元素放入HashEntry数组中。Segment类的put方法是操作的核心,内部回有加锁等机制。源码如下:

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
        // 调用父类ReentrantLock的tryLock()方法尝试锁住segment,
        // 获取锁失败,则调用scanAndLockForPut方法自旋重试获取锁
    HashEntry node = tryLock()?null : scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        HashEntry[] tab = table;
        int index = (tab.length - 1) & hash;
        HashEntry first = entryAt(tab, index);
        for (HashEntry 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(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;
}

当有线程A和线程B在相同segment对象上put对象时,执行过程如下:

1、线程A执行tryLock()方法获取锁。
2、线程B获取锁失败,则执行scanAndLockForPut()方法,在scanAndLockForPut方法中,会通过重复执行tryLock()方法尝试获取锁,在多处理器环境下,重复次数为64,单处理器重复次数为1,当执行tryLock()方法的次数超过上限时,则执行lock()方法挂起线程B;这样设计目的是为了让线程切换和自旋消耗的CPU的时间达到平衡,不至于白白浪费CPU,也不会过于平凡切换线程导致更多的CPU浪费。
3、获得锁之后,根据hash值定位到HashEntry数组的下标,更新或插入元素,在插入过程中,如果HashEntry数组元素个数容量超过负载比例,则进行rehash操作扩容,扩容为原来的两倍(rehash请对比Java集合-HashMap源码实现深入解析中的逻辑,自行分析,基本一模一样);
4、在插入后,还会更新segment的count计数器,用于size方法中计算map元素个数时不用对每个segment内部HashEntry遍历重新计算,提高性能。
5、当线程A执行完插入操作后,会通过unlock()方法释放锁,接着唤醒线程B继续执行;

get方法

get方法比较简单,值得注意的是,get方法不加锁,并用Unsafe.getObjectVolatile方法读取元素,这个方法保证读取对象永远是最新的

public V get(Object key) {
    Segment s; // manually integrate access methods to reduce overhead
    HashEntry[] tab;
    int h = hash(key);
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null &&
        (tab = s.table) != null) {
        for (HashEntry e = (HashEntry) 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;
}

size方法实现

public int size() {
    // Try a few times to get accurate count. On failure due to
    // continuous async changes in table, resort to locking.
    final Segment[] segments = this.segments;
    int size;
    boolean overflow; // true if size overflows 32 bits
    long sum;         // sum of modCounts
    long last = 0L;   // previous sum
    int retries = -1; // first iteration isn't retry
    try {
        for (;;) {
            // 超过重复计算的次数,采用全部加锁后计算
            if (retries++ == RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    ensureSegment(j).lock(); // force creation
            }
            sum = 0L;
            size = 0;
            overflow = false;
            // 汇总每个segment的count计数器
            for (int j = 0; j < segments.length; ++j) {
                Segment seg = segmentAt(segments, j);
                if (seg != null) {
                    sum += seg.modCount;
                    int c = seg.count;
                    if (c < 0 || (size += c) < 0)
                        overflow = true;
                }
            }
            // 判断连续两次计算结果是否相等
            if (sum == last)
                break;
            last = sum;
        }
    } finally {
        // 释放所有segment上的锁
        if (retries > RETRIES_BEFORE_LOCK) {
            for (int j = 0; j < segments.length; ++j)
                segmentAt(segments, j).unlock();
        }
    }
    return overflow ? Integer.MAX_VALUE : size;
}

由于ConcurrentHashMap支持并发读写,所以在准确计算元素时存在一定的难度,思路如下:

1、重复汇总计算segments元素个数,即将每个segment的计数器count累加汇总。
2、如果连续两次计算的sum值相同,则结束计算,认为并发过程中计算的值正确,并返回。
3、如果重复三次,计算的结果都不相同,则强制锁住全部segment后,重新计算值。

虽然采用重复计算和最后加锁的方式再次计算,但size方法仍不能保证结果的准确性,例如,两次计算结果相等,在返回之前,又有新的线程插入新值,则此时的结果就是不准确的。所以,在并发读写环境下,size方法进行精确计算的意义并不大,只能作为一个大概的计算结果,因此也决定了在使用size方法时,要评估清楚自己的需求是什么,是精确计算,还是粗略计算。

Java集合-ConcurrentHashMap工作原理和实现JDK7_第3张图片

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!

你可能感兴趣的:(Java集合-ConcurrentHashMap工作原理和实现JDK7)