欢迎访问昆山宝鼎软件有限公司网站! 设为首页 | 网站地图 | XML | RSS订阅 | 宝鼎邮箱 | 后台管理


新闻资讯

MENU

软件开发知识
原文出处: SylvanasSun's Blog

我们上述所讲的Map都长短线程安详的,这意味着不该该在多个线程中对这些Map举办修改操纵,轻则会发生数据纷歧致的问题,甚至还会因为并发插入元素而导致链表成环(插入会触发扩容,而扩容操纵需要将原数组中的元素rehash到新数组,这时并发操纵就有大概发生链表的轮回引用从而成环),这样在查找时就会发存亡轮回,影响到整个应用措施。

Collections.synchronizedMap(Map<K,V> m)可以将一个Map转换成线程安详的实现,其实也就是通过一个包装类,然后把所有成果都委托给传入的Map实现,并且包装类是基于synchronized要害字来担保线程安详的(时代的眼泪Hashtable也是基于synchronized要害字),底层利用的是互斥锁(同一时间内只能由持有锁的线程会见,其他竞争线程进入睡眠状态),机能与吞吐量差强人意。

public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
    return new SynchronizedMap<>(m);
}
private static class SynchronizedMap<K,V>
    implements Map<K,V>, Serializable {
    private static final long serialVersionUID = 1978198479659022715L;
    private final Map<K,V> m;     // Backing Map
    final Object      mutex;        // Object on which to synchronize
    SynchronizedMap(Map<K,V> m) {
        this.m = Objects.requireNonNull(m);
        mutex = this;
    }
    SynchronizedMap(Map<K,V> m, Object mutex) {
        this.m = m;
        this.mutex = mutex;
    }
    public int size() {
        synchronized (mutex) {return m.size();}
    }
    public boolean isEmpty() {
        synchronized (mutex) {return m.isEmpty();}
    }
    ............
}

然而ConcurrentHashMap的实现细节远没有这么简朴,因此机能也要高上很多。它没有利用一个全局锁来锁住本身,而是回收了淘汰锁粒度的要领,只管淘汰因为竞争锁而导致的阻塞与斗嘴,并且ConcurrentHashMap的检索操纵是不需要锁的。

在Java 7中,ConcurrentHashMap把内部细分成了若干个小的HashMap,称之为段(Segment),默认被分为16个段。对付一个写操纵而言,会先按照hash code举办寻址,得出该Entry应被存放在哪一个Segment,然后只要对该Segment加锁即可。

读操纵仍可<a href=劳务调派信息打点系统 以乱序执行" src="/uploads/allimg/c180914/153DE133MW0-11J8.jpg" />

抱负环境下,一个默认的ConcurrentHashMap可以同时接管16个线程举办写操纵(假如都是对差异Segment举办操纵的话)。

分段锁对付size()这样的全局操纵来说就没有任何浸染了,想要得出Entry的数量就需要遍历所有Segment,劳务派遣管理系统,得到所有的锁,然后再统计总数。事实上,ConcurrentHashMap会先试图利用无锁的方法统计总数,这个实验会举办3次,假如在相邻的2次计较中得到的Segment的modCount次数一致,代表这两次计较进程中都没有产生过修改操纵,那么就可以当做最终功效返回,不然,就要得到所有Segment的锁,从头计较size。

本文主要接头的是Java 8的ConcurrentHashMap,它与Java 7的实现不同较大。完全放弃了段的设计,而是变回与HashMap相似的设计,利用buckets数组与疏散链接法(同样会在高出阈值时树化,对付结构红黑树的逻辑与HashMap不同不大,只不外需要特别利用CAS来担保线程安详),锁的粒度也被细分到每个数组元素(小我私家认为这样做的原因是因为HashMap在Java 8中也实现了不少优化,纵然碰撞严重,也能担保必然的机能,并且Segment不只臃肿尚有弱一致性的问题存在),所以它的并发级别与数组长度相关(Java 7则是与段数相关)。

/**
 * The array of bins. Lazily initialized upon first insertion.
 * Size is always a power of two. Accessed directly by iterators.
 */
transient volatile Node<K,V>[] table;

寻址


ConcurrentHashMap的散列函数与HashMap并没有什么区别,同样是把key的hash code的高16位与低16位举办异或运算(因为ConcurrentHashMap的buckets数组长度也永远是一个2的N次方),然后将扰乱后的hash code与数组的长度减一(实际可会见到的最大索引)举办与运算,得出的功效等于方针地址的位置。

// 2^31 - 1,int范例的最大值
// 该掩码暗示节点hash的可用位,用来担保hash永远为一个正整数
static final int HASH_BITS = 0x7fffffff;
static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}

下面是查找操纵的源码,实现较量简朴。

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        if ((eh = e.hash) == h) {
            // 先实验判定链表头是否为方针,假如是就直接返回
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        else if (eh < 0)
            // eh < 0代表这是一个非凡节点(TreeBin或ForwardingNode)
            // 所以直接挪用find()举办遍历查找
            return (p = e.find(h, key)) != null ? p.val : null;
        // 遍历链表
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}