ConcurrentHashMap 1.7 优化

ConcurrentHashMap(CHM) 从jdk1.5以后引入的高性能的并发map,每一版都有改进,实现方式略有不同,在 java1.8 之前 虽然都是利用锁分段(将锁细化)来达到性能的提升。但是每个版本都在之前的基础上有了优化. 先直接看下官方的介绍。

如下是对1.7的ConcurrentHashMap 介绍。说明了基本的策略是将锁细化,构造时只初始化第一个segments,延迟加载。而对segment以及对segment的table 访问时 使用 volatile  语义。其通过unsafe 中的本地方法实现。而对于在锁操作内的 next字段的写,使用了lazySet (通过putOrderedObject本地方法)写。相比于volatite写,性能提高不少,虽然写完对当下的线程是不会立即可见,但是在unlock后能够保证其可见性。

    /* 
     * The basic strategy is to subdivide the table among Segments, 
     * each of which itself is a concurrently readable hash table.  To 
     * reduce footprint, all but one segments are constructed only 
     * when first needed (see ensureSegment). To maintain visibility 
     * in the presence of lazy construction, accesses to segments as 
     * well as elements of segment's table must use volatile access, 
     * which is done via Unsafe within methods segmentAt etc 
     * below. These provide the functionality of AtomicReferenceArrays 
     * but reduce the levels of indirection. Additionally, 
     * volatile-writes of table elements and entry "next" fields 
     * within locked operations use the cheaper "lazySet" forms of 
     * writes (via putOrderedObject) because these writes are always 
     * followed by lock releases that maintain sequential consistency 
     * of table updates. 
     */  

 

volatite   的  Next指针 

首先在java1.7中,对比1.6最大的不同在于内部数据结构 链表HashEntry 中关于next指针的声明。在1.6中是final类型,而1.7是volatile 型,表明在1.7中next指针是可变的。虽然在1.6中 final型的next能够在高并发情况下,就算一线程 在remove或者put操作时,其它线程 正在 get 或者 遍历 等 不会 带来异常,但是会带来弱一致性。数据不一定是最实时的数据。虽然性能提高了,但是不可避免会导致并发环境中迭代,clear,get等数据的弱一致。

并且在1.6中remove操作会将被删 key之前的结点全部复制一份,并将被删结点前一个结点的next指针指向其下一个结点。导致存在两个链表(新链表,和原链表),如果get操作的 对象是原链表(概率不高),虽然不会带来异常,但是会导致看不到删除结点后的影响 。同时复制链表这样的操作也会带来性能上的损耗。

 

//1.6
static final class HashEntry<K,V> {
    final K key;
    final int hash;
    volatile V value;
    final HashEntry<K,V> next;
    。。。
}

   

//1.7
static final class HashEntry<K, V> {
        final int hash;
        final K key;
        volatile V value;
        volatile ConcurrentHashMap.HashEntry<K, V> next;
        static final Unsafe UNSAFE;
        static final long nextOffset;

        HashEntry(int hash, K key, V value, ConcurrentHashMap.HashEntry<K, V> next) {
       。。。。
        }

        final void setNext(ConcurrentHashMap.HashEntry<K, V> n) {
            UNSAFE.putOrderedObject(this, nextOffset, n);
        }

        static {
            try {
                UNSAFE = Unsafe.getUnsafe();
                Class e = ConcurrentHashMap.HashEntry.class;
                nextOffset = UNSAFE.objectFieldOffset(e.getDeclaredField("next"));
            } catch (Exception var1) {
                throw new Error(var1);
            }
        }
    }

在1.7中HashEntry 源码如上,增加了setNext方法,next指针可变是volatile,而setNext使用了UNSAFE 的本地方法putOrderedObject来更新其值。上面已经说明了 这个操作是lazySet 延时写,而这里之所以使用延时写是为了提升性能,在锁退出时修改自然会更新到内存中,如果采用直接赋值给next字段,由于next时volatile字段,会引起更新直接写入内存而增加开销。

但是这样的话就会导致get 等操作的弱一致性依然存在。

锁的优化

虽然使用的是分段锁,但是不同于1.6中put操作时  直接将  对应的segment   lock,而是在一定时间内自旋,到达一定自旋次数后,而后在lock。 这样做的目的在于不至于使线程频繁的睡眠与唤醒,因为对于大部分的场景,等待锁的时间不会很长,但是线程频繁的睡眠与唤醒会消耗很多的cpu资源。

如下scanAndLockForPut就实现了自旋锁的功能。在自旋的过程中 在节点为空时  还  预创建节点。

put(....){
ConcurrentHashMap.HashEntry node = this.tryLock()?null:this.scanAndLockForPut(key, hash, value);
    .....
}
private ConcurrentHashMap.HashEntry<K, V> scanAndLockForPut(K key, int hash, V value) {
        ConcurrentHashMap.HashEntry first = ConcurrentHashMap.entryForHash(this, hash);
        ConcurrentHashMap.HashEntry e = first;
        ConcurrentHashMap.HashEntry node = null;
        int retries = -1;

        while(!this.tryLock()) {
            if(retries < 0) {
                if(e == null) {
                    if(node == null) {
                        node = new ConcurrentHashMap.HashEntry(hash, key, value, (ConcurrentHashMap.HashEntry)null);
                    }

                    retries = 0;
                } else if(key.equals(e.key)) {
                    retries = 0;
                } else {
                    e = e.next;
                }
            } else {
                ++retries;
                if(retries > MAX_SCAN_RETRIES) {
                    this.lock();
                    break;
                }

                ConcurrentHashMap.HashEntry f;
                if((retries & 1) == 0 && (f = ConcurrentHashMap.entryForHash(this, hash)) != first) {
                    first = f;
                    e = f;
                    retries = -1;
                }
            }
        }

        return node;
    }

全局锁的优化

所有操作中,只涉及到单个Segment的方法,如 get、containsKey、put、putIfAbsent、replace、Remove    这些操作,都是遍历节点链, 而遍历操作是线程安全(通过volatile以及unsafe)。对于前两者只是读操作,不需要加锁。而对于后面的操作,改变了数据,因此需要对本segment加锁后操作,保证只有一个线程 能对本segment做出修改。

而如于涉及多个segment的方法如 size、containsValue、contains、isEmpty 。  

需要访问整个segment数组才能得出想要的结果,因此需要对所有的segment加锁,然而对于全局加锁,必须带来大量的资源消耗,因此现在的实现 会 前不加锁的情况下 执行两次,比较modCount的结果是否一致,因为所有的修改操作都会将modcount++,因此如果在两次执行时间内modCount都 一致的话,表明没有发生变化,否则就需要加锁重新计算。

UNSAFE 优化

java1.7 中大量使用了UNSAFE  中的  putOrderdObject, getObjectVolitate等方法来 更新 获取  Segment[] 以及每个segment内部链表数组 HashEntry[] 。 而UNSAFE提供了很多方法对内存直接进行操作,其实现很底层,可以对应硬件指令。并且编译器能做优化。

如果直接使用volatile 变量 操作,在某些不需要保证可见性的情况下,会降低性能 。因此通过unsafe 对应的本地方法 来确定需要时保证,不需要时可以带来性能优化。

 

1.6中的segment的put 

V put(K key, int hash, V value, boolean onlyIfAbsent) { 
            lock();  // 加锁,这里是锁定某个 Segment 对象而非整个 ConcurrentHashMap 
            try { 
                int c = count; 

                if (c++ > threshold)     // 如果超过再散列的阈值
                    rehash();              // 执行再散列,table 数组的长度将扩充一倍

                HashEntry<K,V>[] tab = table; 
                // 把散列码值与 table 数组的长度减 1 的值相“与”
                // 得到该散列码对应的 table 数组的下标值
                int index = hash & (tab.length - 1); 
                // 找到散列码对应的具体的那个桶
                HashEntry<K,V> first = tab[index]; 

                HashEntry<K,V> e = first; 
                while (e != null && (e.hash != hash || !key.equals(e.key))) 
                    e = e.next; 

                V oldValue; 
                if (e != null) {            // 如果键 / 值对以经存在
                    oldValue = e.value; 
                    if (!onlyIfAbsent) 
                        e.value = value;    // 设置 value 值
                } 
                else {                        // 键 / 值对不存在 
                    oldValue = null; 
                    ++modCount;         // 要添加新节点到链表中,所以 modCont 要加 1  
                    // 创建新节点,并添加到链表的头部 ,可能会导致在get的过程中,value为null,因为new这个对象的时候
                    //  还没有执行完构造函数就被另一个线程得到这个对象引用。所以在get的方法里得到value为null时通过
                    // 加锁,在次获取一次。
                    tab[index] = new HashEntry<K,V>(key, hash, first, value); 
                    count = c;               // 写 count 变量
                } 
                return oldValue; 
            } finally { 
                unlock();                     // 解锁
            } 
        }

get    首先时通过getObjectVolatile确定segment,而后同样通过该方法确定HashEntry。


public V get(Object key) {
    int h = this.hash(key);
    long u = (long)((h >>> this.segmentShift & this.segmentMask) << SSHIFT) + SBASE;
    ConcurrentHashMap.Segment s;
    if((s = (ConcurrentHashMap.Segment)UNSAFE.getObjectVolatile(this.segments, u)) != null) {
        ConcurrentHashMap.HashEntry[] tab = s.table;
        if(s.table != null) {

            for(ConcurrentHashMap.HashEntry e = (ConcurrentHashMap.HashEntry)UNSAFE.getObjectVolatile(tab, ((long)(tab.length - 1 & h) << TSHIFT) + TBASE); e != null; e = e.next) {
                Object k = e.key;
                if(e.key == key || e.hash == h && key.equals(k)) {
                    return e.value;
                }
            }
        }
    }

 

segment的初始化,因为不同于1.6在构造CAS就全部初始化Segment,这里是只初始化一个Segment,后面需要时才通过如下方法初始化。通过UNSAFE的CAS本方方法该map中的Segment的初始化同步。

//初始化  指定的 Segment  
 private ConcurrentHashMap.Segment<K, V> ensureSegment(int k) {
        ConcurrentHashMap.Segment[] ss = this.segments;
        long u = (long)(k << SSHIFT) + SBASE;
        ConcurrentHashMap.Segment seg;
        if((seg = (ConcurrentHashMap.Segment)UNSAFE.getObjectVolatile(ss, u)) == null) {  
            ConcurrentHashMap.Segment proto = ss[0];
            int cap = proto.table.length;
            float lf = proto.loadFactor;
            int threshold = (int)((float)cap * lf);
            ConcurrentHashMap.HashEntry[] tab = (ConcurrentHashMap.HashEntry[])(new ConcurrentHashMap.HashEntry[cap]);
            if((seg = (ConcurrentHashMap.Segment)UNSAFE.getObjectVolatile(ss, u)) == null) {
                ConcurrentHashMap.Segment s = new ConcurrentHashMap.Segment(lf, threshold, tab);

                while((seg = (ConcurrentHashMap.Segment)UNSAFE.getObjectVolatile(ss, u)) == null) {
                    seg = s;
                    if(UNSAFE.compareAndSwapObject(ss, u, (Object)null, s)) {
                        break;
                    }
                }
            }
        }

如下是put操作 在对segment  相关的操作,  分别是根据hash值计算出来的index 来 定位和设置segment中链表。使用UNSAFE 的 getObjectVolatile保证在put时得到最新的头结点。而在设置时又优化了,

static final <K,V> void setEntryAt(HashEntry<K,V>[] tab, int i, HashEntry<K,V> e) {
        UNSAFE.putOrderedObject(tab, ((long)i << TSHIFT) + TBASE, e);   //不保证可见性,性能提升
static final <K,V> HashEntry<K,V> entryAt(HashEntry<K,V>[] tab, int i) {//提供volatile的语义,得到最新的结果
        return (tab == null) ? null : (HashEntry<K,V>) UNSAFE.getObjectVolatile(tab, ((long)i << TSHIFT) + TBASE);
}

总而言之,对于非 volatile变量 的(如Segment[], 局部的tab 等)在获取时,要通过 UNSAFE.getObjectVolatile  本地方法保证 取得最新的结果。

 

总结 :从上面的分析 ,对比1.6 核心策略没有变化 ,但是在性能上有了很大的优化。但是对于put操作并没有解决弱一致性的问题。

 

http://ifeve.com/java-concurrent-hashmap-2/

https://segmentfault.com/q/1010000006669618

http://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/   java1.6

http://www.jianshu.com/p/bd972088a494  java.17

https://segmentfault.com/a/1190000006811416

http://ifeve.com/juc-atomic-class-lazyset-que/   关于 putOrderObject方法的解释

http://www.voidcn.com/blog/patrickyoung6625/article/p-2299054.html  

http://www.importnew.com/22007.html

转载于:https://my.oschina.net/ovirtKg/blog/776582

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值