ConcurrentHashMap的实现原理与使用(二)

上篇已经分析了HashMap在多线程环境下死循环的原因,HashTable使用synchronized来保证线程安全,但是相对来说效率低下,而ConcurrentHashMap是线程安全且高效的HashMap,这第一篇我们来看看ConcurrentHashMap

这篇同样使用上一篇的java环境:


java环境为jdk1.7.0_67

ConcurrentHashMap的结构

ConcurrentHashMap中包含Segment数组,Segment中包含HashEntry数组。

Segment结构

源码如下:

 final Segment<K,V>[] segments;
    ...
 static final class Segment<K,V> extends ReentrantLock implements Serializable {
    ...
    transient volatile HashEntry<K,V>[] table;
    ...
 }

Segment继承了ReentrantLock,ReentrantLock是一个可重入的互斥锁,ReentrantLock的详情以后有时间再聊,在这里简单说一下,ReentrantLock实现了Lock接口,从Java官方API中粘过来说明:A reentrant mutual exclusion Lock
with the same basic behavior and semantics as the implicit monitor lock accessed using synchronized methods and statements, but with extended capabilities.在这里翻一下(英文不好,强行使用百度翻译加上自己组织):一个可重入的互斥锁,和关键词synchronized修饰的方法与语句访问隐式监视锁(可能翻译错了,就理解为和synchronized具有相同作用吧)具有相同的功能和语义,但具有扩展功能,翻译完毕。那么我们可以分析出Segment在ConcurrentHashMap作为锁,保证了ConcurrentHashMap的线程安全。

HashEntry结构

HashEntry是一个单链表结构,使用HashEntry<K,V>键值对存数据,next节点指向下一个节点,与HashMap不同的是,存值的value变量、存下一个节点的变量next使用了关键字volatile(volatile 自行百度吧)修饰,源码如下:

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;
        }
        
        ...
    }
分段锁技术保证了线程安全并提高了ConcurrentHashMap的并发访问率

通过分析了Segment、HashEntry结构与源码,可以得出,因为Segment继承了ReentrantLock,所以ConcurrentHashMap使用了分段锁的技术。把数据分为一段一段,也就是Segment数组,每个Segment中都有一个HashEntry数组,当对HashEntry进行数据存与读的时候,先要获取与之相对应的Segment锁,这样当多线程环境下,一个线程获得锁,访问这一段的数据的时候,其他线程也可以访问其他段的数据,所以保证了线程安全的同时提高了ConcurrentHashMap的并发访问率。


灵魂画师这次使用了做图工具

ConcurrentHashMap的put、get方法

分析一下ConcurrentHashMap的put、get方法源码

put方法

先算出数据要存到哪段中,通过算法去定位Segment,然后调用Segment对象的put方法去存储数据

public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        //hash算法算出key的哈希值
        int hash = hash(key);
        //通过算法算出数据该存到哪一段上
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
                (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            //定位Segment数组中的Segment
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
    }

再来看Segment的put方法

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
        //先对当前的Segment的进行加锁,保证线程安全
        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;
                    //判断是否需要HashEntry是否需要扩容
                    if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                        rehash(node);
                    else
                        setEntryAt(tab, index, node);
                    ++modCount;
                    count = c;
                    oldValue = null;
                    break;
                }
            }
        } finally {
            unlock();
        }
        return oldValue;
    }

一进到Segment的put方法,进行了加锁保证了线程安全,再添加的过程中,判断是否需要扩容,扩容过程中也会进行数据转存,但是已经进行了加锁,所以不会再发生HashMap中死循环的现象,而且,扩容不是针对于整个ConcurrentHashMap容器的扩容,而是针对于某个Segment中的HashEntry数组进行了扩容,这样提高了ConcurrentHashMap的效率。

get方法

先通过算法去定位Segment,然后在通再算法定位元素

public V get(Object key) {
        Segment<K,V> s; // manually integrate access methods to reduce overhead
        HashEntry<K,V>[] tab;
        int h = hash(key);
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
                (tab = s.table) != null) {
            for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                    (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
                 e != null; e = e.next) {
                K k;
                if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                    return e.value;
            }
        }
        return null;
    }

get过程没有加锁,保证了高效,但是怎么保证线程安全的呢?记得上面分析Segment结构与HashEntry结构的时候,Segment中HashEntry数组变量table使用了volatile修饰,HashEntry中用来存值的value变量也使用了volatile修饰,保证了table变量与value变量在线程间的相互可见性,就算是多个线程修改了HashEntry中的value,get方法也能读取到value在内存中的最新值,所以既保证了线程安全又保证了高效。

ConcurrentHashMap的实现原理与使用是说完了。
欢迎大家来交流,指出文中一些说错的地方,希望大家多多提出,让我加深认识。
谢谢大家!

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

推荐阅读更多精彩内容