线程安全的并发容器1.7版本的ConcurrentHashMap原理分析(二十二)

线程安全的并发容器1.7版本的ConcurrentHashMap原理分析:

ConcurrentHashMap 使用
除了 Map 系列应该有的线程安全的 get put 等方法外, ConcurrentHashMap 还提供了一个在并发下比较有用的方法 putIfAbsent ,如果传入 key 对应的 value 已经存在,就返回存在的 value ,不进行替换。如果不存在,就添加 key value , 返回 null 。在代码层面它的作用类似于:
synchronized(map){
if (map.get(key) == null){
    return map.put(key, value);
} else{
    return map.get(key);
    }
}

源码如下:

public V putIfAbsent(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)UNSAFE.getObject
             (segments, (j << SSHIFT) + SBASE)) == null)
            s = ensureSegment(j);
        return s.put(key, hash, value, true);
    }

它让上述代码的整个操作是线程安全的。

1.7 下的实现
 
1,
桶:
static final class Segment<K,V> extends ReentrantLock implements Serializable {
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。 Segment 是一种可重入锁( ReentrantLock ),在 ConcurrentHashMap 里扮演锁的 角色;HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个 Segment 数组。 Segment 的结构和 HashMap 类似,是一种数组和链表结构。一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素, 每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据 进行修改时,必须首先获得与它对应的 Segment 锁。
2、 构造方法和初始化
public ConcurrentHashMap17(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;
        }
        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;
        while (cap < c)
            cap <<= 1;
        // create segments and segments[0]
        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;
    }
ConcurrentHashMap 初始化方法是通过 initialCapacity loadFactor 和 concurrencyLevel(参数 concurrencyLevel 是用户估计的并发级别,就是说你觉得最 多有多少线程共同修改这个 map ,根据这个来确定 Segment 数组的大小 concurrencyLevel 默认是 DEFAULT_CONCURRENCY_LEVEL = 16;) 等几个参数来初始 化 segment 数组、段偏移量 segmentShift 、段掩码 segmentMask 和每个 segment 里的 HashEntry 数组来实现的。并发级别可以理解为程序运行时能够同时更新 ConccurentHashMap 且不产 生锁竞争的最大线程数,实际上就是 ConcurrentHashMap 中的分段锁个数,即 Segment[]的数组长度。 ConcurrentHashMap 默认的并发度为 16 ,但用户也可以 在构造函数中设置并发度。当用户设置并发度时,ConcurrentHashMap 会使用大 于等于该值的最小 2 幂指数作为实际并发度(假如用户设置并发度为 17 ,实际 并发度则为 32 )。 如果并发度设置的过小,会带来严重的锁竞争问题;如果并发度设置的过大, 原本位于同一个 Segment 内的访问会扩散到不同的 Segment 中, CPU cache 命中 率会下降,从而引起程序性能下降。(文档的说法是根据你并发的线程数量决定, 太多会导性能降低) segments 数组的长度 ssize 是通过 concurrencyLevel 计算得出的。为了能通 过按位与的散列算法来定位 segments 数组的索引,必须保证 segments 数组的长 度是 2 N 次方( power-of-two size ),所以必须计算出一个大于或等于 concurrencyLevel 的最小的 2 N 次方值来作为 segments 数组的长度。假如 concurrencyLevel 等于 14 15 16 ssize 都会等于 16 ,即容器里锁的个数也是 16。
以下知识了解即可:
初始化 segmentShift segmentMask 这两个全局变量需要在定位 segment 时的散列算法里使用, sshift 等于 ssize 1 向左移位的次数,在默认情况下 concurrencyLevel 等于 16 1 需要向左移位 移动 4 次,所以 sshift 等于 4 segmentShift 用于定位参与散列运算的位数, segmentShift 等于 32 sshift ,所以等于 28 ,这里之所以用 32 是因为 ConcurrentHashMap 里的 hash() 方法输出的最大数是 32 位的。 segmentMask 是散 列运算的掩码,等于 ssize 1 ,即 15 ,掩码的二进制各个位的值都是 1 。因为
ssize 的最大长度是 65536 ,所以 segmentShift 最大值是 16 segmentMask 最大值 65535 ,对应的二进制是 16 位,每个位都是 1
 
输入参数 initialCapacity 是 ConcurrentHashMap 的初始化容量,loadfactor 是 每个 segment 的负载因子,在构造方法里需要通过这两个参数来初始化数组中的 每个 segment 。上面代码中的变量 cap 就是 segment HashEntry 数组的长度, 它等于 initialCapacity 除以 ssize 的倍数 c ,如果 c 大于 1 ,就会取大于等于 c 2 的 N 次方值,所以 segment HashEntry 数组的长度不是 1 ,就是 2 N 次方。 在默认情况下, ssize = 16 initialCapacity = 16 loadFactor = 0.75f ,那么 cap = 1, threshold = (int) cap * loadFactor = 0 既然 ConcurrentHashMap 使用分段锁 Segment 来保护不同段的数据,那么在 插入和获取元素的时候,必须先通过散列算法定位到 Segment 。 ConcurrentHashMap 会首先使用 Wang/Jenkins hash 的变种算法对元素的 hashCode 进行一次再散列。 ConcurrentHashMap 完全允许多个读操作并发进行,读操作并不需要加锁。 ConcurrentHashMap 实现技术是保证 HashEntry 几乎是不可变的以及 volatile 关键 字。
 
3、HashEntry结构:
static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;

        HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        /**
         * Sets next field with volatile write semantics.  (See above
         * about use of putOrderedObject.)
         */
        final void setNext(HashEntry<K,V> n) {
            UNSAFE.putOrderedObject(this, nextOffset, n);
        }

        // Unsafe mechanics
        static final sun.misc.Unsafe UNSAFE;
        static final long nextOffset;
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class k = HashEntry.class;
                nextOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("next"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }
4、 get 操作
public V get(Object key) {
        Segment<K,V> s; // manually integrate access methods to reduce overhead
        HashEntry<K,V>[] tab;
        int h = hash(key);//准备定位的hash值
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
            (tab = s.table) != null) {//拿到segment下的table
            for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                     (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
                 e != null; e = e.next) {//遍历table下指定的HashEntry列表
                K k;
                if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                    return e.value;
            }
        }
        return null;
    }
get 操作先经过一次再散列,然后使用这个散列值通过散列运算定位到 Segment(使用了散列值的高位部分 ) ,再通过散列算法定位到 table( 使用了散列值 的全部) 。整个 get 过程,没有加锁,而是通过 volatile 保证 get 总是可以拿到最 新值。
transient volatile HashEntry<K,V>[] table;
5、 put 操作
public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);//定位所需的hash值
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);//初始化segment[j],因为整个map初始化时,只初始化了segment[0]
        return s.put(key, hash, value, false);
    }
ConcurrentHashMap 初始化的时候会初始化第一个槽 segment[0] ,对于其他 槽,在插入第一个值的时候再进行初始化。 ensureSegment 方法考虑了并发情况,多个线程同时进入初始化同一个槽 segment[k],但只要有一个成功就可以了。 具体实现是
private Segment<K,V> ensureSegment(int k) {
        final Segment<K,V>[] ss = this.segments;
        long u = (k << SSHIFT) + SBASE; // raw offset
        Segment<K,V> seg;
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
            Segment<K,V> proto = ss[0]; // use segment 0 as prototype
            int cap = proto.table.length;
            float lf = proto.loadFactor;
            int threshold = (int)(cap * lf);
            HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
            if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                == null) { // recheck
                Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
                //多次检查,循环CAS操作,保证多线程下只有一个线程可以成功
                while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                       == null) {
                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                        break;
                }
            }
        }
        return seg;
    }

最终调用s.put(key, hash, value, false);方法:

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            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<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;
        }
put 方法会通过 tryLock() 方法尝试获得锁,获得了锁, node null 进入 try 语句块,没有获得锁,调用 scanAndLockForPut 方法自旋等待获得锁。
 
6、调用  scanAndLockForPut(key, hash, value)方法:
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
            HashEntry<K,V> first = entryForHash(this, hash);
            HashEntry<K,V> e = first;
            HashEntry<K,V> node = null;
            int retries = -1; // negative while locating node
            while (!tryLock()) {
                HashEntry<K,V> f; // to recheck first below
                if (retries < 0) {
                    if (e == null) {
                        if (node == null) // speculatively create node
                            node = new HashEntry<K,V>(hash, key, value, null);
                        retries = 0;
                    }
                    else if (key.equals(e.key))
                        retries = 0;
                    else
                        e = e.next;
                }
                else if (++retries > MAX_SCAN_RETRIES) {
                    lock();
                    break;
                }
                else if ((retries & 1) == 0 &&
                         (f = entryForHash(this, hash)) != first) {
                    e = first = f; // re-traverse if entry changed
                    retries = -1;
                }
            }
            return node;
        }
scanAndLockForPut 方法里在尝试获得锁的过程中会对对应 hashcode 的链表 进行遍历,如果遍历完毕仍然找不到与 key 相同的 HashEntry 节点,则为后续的 put 操作提前创建一个 HashEntry 。当 tryLock 一定次数后仍无法获得锁,则通过 lock 申请锁。在获得锁之后,Segment 对链表进行遍历,如果某个 HashEntry 节点具有相 同的 key ,则更新该 HashEntry value 值,否则新建一个 HashEntry 节点,采用头插法,将它设置为链表的新 head 节 点并将原头节点设为新 head 的下一个节点。新建过程中如果节点总数(含新建 的 HashEntry )超过 threshold ,则调用 rehash() 方法对 Segment 进行扩容,最后 将新建 HashEntry 写入到数组中。
 
7、 rehash 操作
put方法里 rehash(node);
private void rehash(HashEntry<K,V> node) {
          
            HashEntry<K,V>[] oldTable = table;
            int oldCapacity = oldTable.length;
            int newCapacity = oldCapacity << 1;/* oldCapacity*2 */
            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;
        }

扩容是新创建了数组,然后进行迁移数据,最后再将 newTable 设置给属性table。为了避免让所有的节点都进行复制操作:由于扩容是基于 2 的幂指来操作, 假设扩容前某 HashEntry 对应到 Segment 中数组的 index i,数组的容量为 capacity,那么扩容后该 HashEntry 对应到新数组中的 index 只可能为 i 或者 i+capacity,因此很多HashEntry 节点在扩容前后 index 可以保持不变。

假设原来 table 长度为 4 ,那么元素在 table 中的分布是这样的
 
扩容后 table 长度变为 8 ,那么元素在 table 中的分布变成:
可以看见 hash 值为 34 56 的下标保持不变,而 15,23,77 的下标都是在原 来下标的基础上+4 即可,可以快速定位和减少重排次数。 该方法没有考虑并发,因为执行该方法之前已经获取了锁。
8、 remove 操作 :key做参数
public V remove(Object key) {
        int hash = hash(key);
        Segment<K,V> s = segmentForHash(hash);
        return s == null ? null : s.remove(key, hash, null);
    }

或:用key和value做参数

public boolean remove(Object key, Object value) {
        int hash = hash(key);
        Segment<K,V> s;
        return value != null && (s = segmentForHash(hash)) != null &&
            s.remove(key, hash, value) != null;
    }

与 put 方法类似,都是在操作前需要拿到锁,以保证操作的线程安全性。

 
9、 ConcurrentHashMap 的弱一致性
 
然后对链表遍历判断是否存在 key 相同的节点以及获得该节点的 value 。但 由于遍历过程中其他线程可能对链表结构做了调整,因此 get containsKey 返 回的可能是过时的数据,这一点是 ConcurrentHashMap 在弱一致性上的体现。如 果要求强一致性,那么必须使用 Collections.synchronizedMap() 方法。
size containsValue
这些方法都是基于整个 ConcurrentHashMap 来进行操作的,他们的原理也基 本类似:首先不加锁循环执行以下操作:循环所有的 Segment ,获得对应的值以 及所有 Segment modcount 之和。在 put remove clean 方法里操作元素 前都会将变量 modCount 进行变动,如果连续两次所有 Segment modcount 和相等,则过程中没有发生其他线程修改 ConcurrentHashMap 的情况,返回获得 的值。当循环次数超过预定义的值时,这时需要对所有的 Segment 依次进行加锁, 获取返回值后再依次解锁。所以一般来说,应该避免在多线程环境下使用 size 和 containsValue 方法。
今天主要分享1.7版本的 ConcurrentHashMap,下篇我们分享1.8版本的原理,敬请期待!
 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

寅灯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值