ConcurrentHashMap如何实现高效线程

Java集合线程安全机制

Java提供了不同层面的线程安全支持。在传统集合框架内部,除了HashTable等同步容器,还提供了所谓的同步包装器(synchronized Wrapper),可以通过调用
Collection工具类的包装方法,来获取同步包装器(Collection.synchronizeList,Collection.synchronizeMap等),但是它们都是利用非常粗粒度的同步方式,
在高并发情况下,性能比较低下。
更普遍的使用利用Java并发库中的线程安全容器类,它提供如下工具类:
各种并发容器(如concurrentHashMap,CopyOnWriteArrayList等)
各种线程安全队列(queue/deque),如ArrayBlockingQueue,SynchronousQueue
各种有序容器的安全版本等。
具体保证线程安全的方式,既包括简单synchronized方式,有基于更精细化的并发实现(如基于分离锁实现的concurrentHash)

理解基本线程安全工具

低效同步实现方法

hashtable本身比较低效,同步实现基本是在put、get、size的方法上添加synchronized,简单说,导致所有并发操作去竞争同一把锁,通过互斥、排他进行资源访问
其他线程只能中断对待,而且不能做其他事情(RetrantLock的中断等待,可以在无法获取资源持有锁时,无需一致等待,转而做其他事情),大大降低并发操作效率

Collections的同步包装器虽然没有synchronized方法,但是其还是利用this作为互斥的mutex,构造了另一个同步版本,进行互斥、排他访问,无真正意义改进

private static class SynchronizedMap<K,V>
    implements Map<K,V>, Serializable {
    private final Map<K,V> m;     // Backing Map
    final Object      mutex;        // Object on which to synchronize
    // …
    public int size() {
        synchronized (mutex) {return m.size();}
    }
 // … 
}
故HashTable,Collections同步包装器版本不适合在高并发场景使用

(3)传统集合Map并发编程时存在的问题及不足

HashTable通过synchronized方法来实现并发访问,让所有并发操作竞争同一把锁,通过互斥、排他进行资源访问
其他线程只能中断对待,而且不能做其他事情(RetrantLock的中断等待,可以在无法获取资源持有锁时,无需一致等待,转而做其他事情),大大降低并发操作效率
Collections的同步包装器虽然没有synchronized方法,但是其还是利用this作为互斥的mutex,构造了另一个同步版本,进行互斥、排他访问,无真正意义改进

(4)并发库(尤其concurrentHashMap)采用哪些方法来提高并发表现

尤其concurrentHashMap,jdk7以前使用分段锁对map部分数组对应的链表进行同步操作,volatile value保证线程间操作可见性
使用Unsafe的CAS,提供硬件级别的原子操作,实现无锁同步逻辑

(5)掌握concurrentHashMap自身演进

结构

1.ConcurrentHashMap内部是由以HashEntry(key,value对)组成的segment构成,Segment数量由concurrentcyLevel决定,默认是16,也可以在幸运构造函数直接指定。必须是2的幂数,否则会被调整接近其值得2的幂数级的值。
其get方法,通过声明volatile值来保证值在线程间可见性,无同步连接
2.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.hashCode());
       //利用位操作替换普通数学运算
       long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
        // 以Segment为单位,进行定位
        // 利用Unsafe直接进行volatile access
        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
            (tab = s.table) != null) {
           //省略
          }
        return null;
    }

3.put方法
首先通过二次hash避免hash冲突,然后以Unsafle调用方式,直接获取相应的Segment(内存的变量value,旧的变量预期值,相等则通过硬件级别原子2操作,将变量改为新的值),然后进行现场安全put操作
从下面源码看到获取再入锁,以保证数据一致性,Segment(分段锁)本身就是基于ReentrantLock的扩展实现,所以并发修改期间,相应的Segment是被锁定的。
最初阶段,进行重复性扫描,以确定相应key值是否在已经在数组里面,进而决定是否更新还是放置操作。重复扫描、检测冲突时ConcurrentHash常见技巧
扩容问题在ConcurrentHash同样存在,明显区别是:不进行整体扩容,单独对Segment进行扩容。

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            // scanAndLockForPut会去查找是否有key相同Node
            // 无论如何,确保获取锁
            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;
                        // 更新已有value...
                    }
                    else {
                        // 放置HashEntry到特定位置,如果超过阈值,进行rehash
                        // ...
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }


4.size()方法涉及分段锁的副作用:若不进行同步,简单计算所有segment的总值,会因并发的put操作,导致总值不准确,但是直接锁定所有segment,又与Collections的同步包装器,开销昂贵
并且分段锁也限制Map初始化操作。故ConcurrentHashMap实现是通过重试机制(RETRIES_BEFORE_LOCK,指定重试次数为2)来获取可靠值,如果对比Segment.modCount监控无变化,则直接返回

演进

ConcurrentHashMap设计一直在演化。如Java8发生非常大变化(Java7 也有不少更新),可以从结构、实现机制等方面,对比不同版本的区别
早期ConcurrentHashMap实现基于
分离锁:将内部进行分段(Segment),里面是HashEntry(将若干数组元素对应的链表作为一个HashEntry),和HashMap类似,hash相同的条目也已链表形式存储
进行并发操作时,只锁定相应段,避免hashTable整体同步的问题,大大提高性能
Volatile value:HashEntry内部使用volatile的value字段来保证可见性,也利用不可变对象机制以改进利用Unsafe提供底层能力,如volatile access,去直接完成部分操作。
以最佳性能,毕竟Unsafe操作都是JVM intrinsic内部机制进行优化过的。

Java8对应变化

结构

总体结构,它的内部存储和HashMap结构非常相似,同样是大的桶(bucket),然后内部也是一个个所谓的链表结构(bin),同步的力度更细致一些
其内部仍有Segment定义,仅仅为了保证序列化兼容,无任何结构上用处
不再使用Segment,初始化操作大大简化,修改为Lazy-load形式,这样可以有效避免初始开销,解决一开始就分配空间的问题
数据存储volatile保证可见性
使用CAS等操作,在特定场景进行无锁并发操作
使用Unsafe,LongAdder之类底层手段,进行极端情况的优化

源码分析

1.内部实现:key是final,其生命周期内,key不可发生改变,val是volatile,保证线程间访问的可见性

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            // scanAndLockForPut会去查找是否有key相同Node
            // 无论如何,确保获取锁
            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;
                        // 更新已有value...
                    }
                    else {
                        // 放置HashEntry到特定位置,如果超过阈值,进行rehash
                        // ...
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }

2.put()方法
下述代码中,因为现在jdk中synchronized已经不断被优化,可以不再担心性能差异,同步逻辑用的synchronized代码块,相比ReentrantLock可以减少内存消耗,是个非常大优势
final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh; K fk; V fv;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 利用CAS去进行无锁线程安全操作,如果bin是空的
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                break; 
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else if (onlyIfAbsent // 不加锁,进行检查
                 && fh == hash
                 && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                 && (fv = f.val) != null)
            return fv;
        else {
            V oldVal = null;
            synchronized (f) {
                   // 细粒度的同步修改操作... 
                }
            }
            // Bin超过阈值,进行树化
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

3.initTable()方法:
初始化实现在initTable(),典型CAS使用场景,利用volatile的sizeCtl作为互斥手段:如发现竞争性初始化,就spin在哪里等条件恢复;否则就利用CAS设置排他标志,
如果成功,则进行初始化;否则重试
当bin为空时,同样不需锁定,也是CAS操作放置,
private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        // 如果发现冲突,进行spin等待
        if ((sc = sizeCtl) < 0)
            Thread.yield(); 
        // CAS成功返回true,则进入真正的初始化逻辑
        else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

4.tabAt()方法
利用Unsafe进行优化,tabAt()直接利用getObjectAcquire()避免间接调用的开销

static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    return (Node<K,V>)U.getObjectAcquire(tab, ((long)i << ASHIFT) + ABASE);
}



5.size():统计集合实际元素总数
真正逻辑在sumCount(),其思路与jdk7及其以前类似,都是分而治之的进行计数,然后求和处理,但实现是CounterCell,是Java并发库类似原子操作类(LongAdder)
是JVM利用空间换时间的更高效的方法,利用了Striped64内部的复杂逻辑。大多数下利用AtomicLong进行原子操作即可满足要求。

final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}
static final class CounterCell {
    volatile long value;
    CounterCell(long x) { value = x; }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值