ConcurrentHashMap(jdk1.6版本,后续改了很多了!!)

存储结构

分为多个segment,每个segment和hashmap结构一样(一个table+拉链),
每个segment单独管理自己的count。

类声明

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>  
        implements ConcurrentMap<K, V>, Serializable {  
}

concurrentmap

提供几个复合源自操作:

  • putIfAbsent(K,V), 没有则添加
  • remove(K,V) 有条件删除,如果待删除的key的值和传入的V一致,则删除
  • replace(K,V), 如果有值,才替换(和putIfAbsent相反)
  • replace(K,oldV,NewV) 。如果有值且旧值等于oldV才替换

Segment

每个segment都是一个独立的锁,独立维护自己的count,拥有和hashmap差不多的基本操作,一般的get,put,remove等操作,都是委托segment进行的。

static final class Segment<K,V> extends ReentrantLock implements Serializable { //继承自ReentrantLock 
    transient volatile int count; // 独立维护的count
    transient int modCount;
    transient int threshold;
    transient volatile HashEntry<K,V>[] table;
    final float loadFactor;
}

segment的加入使得concurrenthashmap实现了锁分离,默认并发数为16,光这一点就足以使得它比hashmap并发性能高出十几倍。

concurrenthashmap的get不加锁却能保证线程安全

Segment中具体数据是维护在一个HashEntry<K,V> table中的。首先看一下hashentry的定义:

/*
 非典型不可变对象(value可变)
*/
static final class HashEntry<K,V> {  
     final K key;   // final 
     final int hash;  // final,hash值已经被缓存
     volatile V value;  // volatile保证更新可见性
     final HashEntry<K,V> next;   //不可变,所以增删操作只能在链表头结点处进行!!
 }  

<b>为什么get不加锁却能线程安全???以下按照各种情况一条一条分析</b>
get线程安全前提是什么?其他线程在进行增删改操作时不影响读线程。

巧妙的使用了volatile和不可变对象!!!!!

  • a) put操作,如果hashenrty中当前key已经存在,只是更新value为新值,由于value为volatile类型,JMM可以保证更新value操作happen-before于所有同时读的操作!

  • b) put操作,key不存在,需要新增节点。concurrenthashmap会直接在table对应头结点位置处新增一个节点,并指向老的头结点。此时读线程如果已经获取到了老的头结点,并不影响它继续遍历get。

  • c) remove操作,会先把remove掉的节点前面的链表拷贝一份并拼接到被删除节点的后续节点上。如下:
    删除前:bucket->a->b->c->d->e->f
    删除掉c: bucket->b->a->d->e->f
    拷贝后前置节点的顺序会反过来。
    此时如果有读线程读到了c节点,而另外的线程把c节点删了完全不会影响读线程继续读操作。只是读线程本次读入可能会读到已经被删除的值。

  • d) clear操作, 只是把所有table的头节点置空,读线程如果读到了原来的节点,还是可以继续自己的查找。

<b>这种get请求不加锁的方式,没有办法完全的实时性保证,被remove的节点可能会被读到(写线程删除某个节点的同时,读线程已经开始遍历)。新增的节点可能没有第一时间读到值(新增的同时,读线程已经获取到旧的头结点开始遍历查找了)。</b>,其实这种不是特别及时的读没太大关系,因为读到的值对map本身不会造成影响,如果需要先读取值再使用该值对map进行操作,需要直接使用concurrentmap提供的复合原子操作。

get方法源码:

V get(Object key, int hash) {
        if (count != 0) { // read-volatile // ①//这里可以去掉吗??不可以,count的读保证了happen-before原则
            HashEntry<K,V> e = getFirst(hash); 
            while (e != null) {
                if (e.hash == hash && key.equals(e.key)) {
                    V v = e.value;
                    if (v != null)  // ② 注意这里
                        return v;
                    return readValueUnderLock(e); // recheck
                }
                e = e.next;
            }
        }
        return null;
}
  • 首先,判断count是否大于0,count为volatile,能保证判断时即使有更新操作也能及时可见。
  • 如果读到的值为null,加锁重读。

<b>为什么读到的值会可能为null?</b>
这就是对象的安全发布问题了,hashentry除了value外都是final类型,jmm能保证他们的安全初始化,完全属于不可变对象,所以读取这些属性都不会为null,但是1.value不是final类型,2.hashentry对象在segment中并没有按照安全发布对象的方式发布, 3.concurrenthashmap本身就不支持值为null(我在想可能就是因为这里不好区分吧,如果用户本来就是传入的null呢~~~)。所以这里如果为null就是hashentry还没有构造完成(此时可能写线程正在构造中),所以想要获取真实的value值,就必须等待写线程将hashentry构造完成,所以就必须加锁等待。

V readValueUnderLock(HashEntry<K,V> e) {  
     lock();   
     try {  
         return e.value;  
     } finally {  
         unlock();  
     }  
 }

所以,只有当get操作获取到一个并未构造完成的对象值时,才需要加锁。

size,isEmpty,contains,containsValue等需要全局查找(遍历所有segment)的操作如何实现的。

  • size:执行最多RETRIES_BEFORE_LOCK次累加segment中count值操作,并同时累加并记录下来统计前每一个segment的modcount,count累加结束后判断modcount总值或者每一个segment的modcount是否有改变,如果变了则从新再来。如果RETRIES_BEFORE_LOCK次周而复始都没有得到稳定的结果,则进行全局segment加锁,并统计count操作!
    <b>因为count是volatile,所以虽然modCount不是volatile类型,但是由于count的存在,使得modCount也可见了,为什么呐?因为count肯定是遵循happen-before原则的,而map源码本身保证所有的结构更新操作都会在操作的最后一步执行更新count,所以hb(Segment内部结构更新,count更新)[单线程内的happen-before原则],然后hb(count更新,count读),按照传递性hb(Segment内部结构更新,count读),所以执行了count读后,后续对segment内部不管是modCount还是内部的bucket,都是能看到刚刚更新的新值!!!!!!!</b>
    public int size() {
        final Segment<K,V>[] segments = this.segments;
        long sum = 0;
        long check = 0;
        int[] mc = new int[segments.length];
        // Try a few times to get accurate count. On failure due to
        // continuous async changes in table, resort to locking.
        for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) {
            check = 0;
            sum = 0;
            int mcsum = 0;
            for (int i = 0; i < segments.length; ++i) {
                sum += segments[i].count;
                mcsum += mc[i] = segments[i].modCount;
            }
            if (mcsum != 0) {
                for (int i = 0; i < segments.length; ++i) {
                    check += segments[i].count;
                    if (mc[i] != segments[i].modCount) {
                        check = -1; // force retry
                        break;
                    }
                }
            }
            if (check == sum)
                break;
        }
        if (check != sum) { // Resort to locking all segments
            sum = 0;
            for (int i = 0; i < segments.length; ++i)
                segments[i].lock();
            for (int i = 0; i < segments.length; ++i)
                sum += segments[i].count;
            for (int i = 0; i < segments.length; ++i)
                segments[i].unlock();
        }
        if (sum > Integer.MAX_VALUE)
            return Integer.MAX_VALUE;
        else
            return (int)sum;
    }
  • containsValue
    public boolean containsValue(Object value) {
        if (value == null)
            throw new NullPointerException();

        // See explanation of modCount use above

        final Segment<K,V>[] segments = this.segments;
        int[] mc = new int[segments.length];

        // Try a few times without locking
        for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) {
            int sum = 0;
            int mcsum = 0;
            for (int i = 0; i < segments.length; ++i) {
                int c = segments[i].count;
                mcsum += mc[i] = segments[i].modCount;
                if (segments[i].containsValue(value))
                    return true;
            }
            boolean cleanSweep = true;
            if (mcsum != 0) {
                for (int i = 0; i < segments.length; ++i) {
                    int c = segments[i].count; // 特别注意
                    if (mc[i] != segments[i].modCount) {
                        cleanSweep = false;
                        break;
                    }
                }
            }
            if (cleanSweep)
                return false;
        }
        // Resort to locking all segments
        for (int i = 0; i < segments.length; ++i)
            segments[i].lock();
        boolean found = false;
        try {
            for (int i = 0; i < segments.length; ++i) {
                if (segments[i].containsValue(value)) {
                    found = true;
                    break;
                }
            }
        } finally {
            for (int i = 0; i < segments.length; ++i)
                segments[i].unlock();
        }
        return found;
    }

特别注意int c = segments[i].count;这句话,只是为了获取volatile的count以保证modCount也能获取到实时更新的值。

迭代器

和get操作一样,弱一致性迭代器,为了保证get,put等高频操作的高并发性!弱一致是指在迭代过程中如果有元素被删除了或者新增了可能不会在迭代过程中反映出来。concurrenthashmap的迭代器不会抛fast-fail异常!

总结

concurrenthashmap的高并发性支撑在:

  1. 锁分离,不同segment不同的锁
  2. 读操作不需要加锁
  3. size,isempty等全map遍历操作做了一定程度优化。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354

推荐阅读更多精彩内容