ConcurrentHashMap源码分析(02)-putVal()方法

前言

上一章节,我们对构造方法进行了分析,接下来我们要分析的是元素的插入。在Map接口的方法定义里面,put()方法的职责就是插入元素。而Concurrent.put()方法又直接调用了putVal()方法,那么我们分析的重点将放在putVal()上面。

put()方法

我们先来看看put()方法是否如前言所描述那般。根据下面的源码,我们得到了证明。

public V put(K key, V value) {
    return putVal(key, value, false);
}

putVal()方法

先来看看这个方法的源码。

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 1. 如果key或者value为null,抛出空指针异常,这点和`HashMap`有所不同
    if (key == null || value == null) throw new NullPointerException();
    // 2. 根据key的hashCode(),重新生成hash
    int hash = spread(key.hashCode());
    int binCount = 0;
    // 3. 这儿是空循环,也就是自旋操作
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // 3.1. 如果`table`为null或长度为0,需要对`table`进行初始化。从上一节构造方法的分析来看,构造方法并不会初始化`table`。初始化`table`延迟到`putVal()`方法
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        // 3.2. 如果hash没有冲突,则使用CAS插入
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        // 3.3. 如果hash冲突了,且槽里面第一个元素的hash为MOVED(-1)。则说明槽里面的元素为`ForwardingNode`对象,有其他线程正在扩容。当前线程通过`helpTransfer`进行辅助扩容,扩容完成之后再进行插入操作
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        // 3.4. 如果hash冲突了,且槽里面第一个元素的hash不为-1,需要对槽里面首元素进行加锁,然后将元素加进去
        else {
            V oldVal = null;
            // 对首元素加锁,相比于jdk7来说,又有了进一步的性能提升
            synchronized (f) {
                // 这儿需要再次判断槽里面的首元素是否为f。因为在添加元素的时候有可能同时又有其他线程在移除元素,这种情况需要自旋来重新插入
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) { // 槽里面的首元素是f,且首元素的hash是非负数,说明槽的数据结构为链表,依次遍历插入即可
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) { // 槽里的首元素是f,且首元素是TreeBin结构,说明槽的数据结构为红黑树,则需要将元素插入到红黑树。红黑树的插入可能会进行左右旋等操作
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            // binCount表示槽里面元素的个数,如果个数>=8,则需要将链表结构转换为红黑树结构,以此来提高性能
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 4. 通过`addCount()`方法进行扩容
    addCount(1L, binCount);
    return null;
}

我们来分析一下这个方法,该方法做了下面这些事情

  1. 如果key或者value为null,抛出空指针异常,这点和HashMap有所不同
  2. 根据key的hashCode(),重新生成hash
  3. 这儿是空循环,也就是自旋操作
    1. 如果table为null或长度为0,需要对table进行初始化。从上一节构造方法的分析来看,构造方法并不会初始化table。初始化table延迟到putVal()方法
    2. 如果hash没有冲突,则使用CAS插入
    3. 如果hash冲突了,且槽里面第一个元素的hash为MOVED(-1)。则说明槽里面的元素为ForwardingNode对象,有其他线程正在扩容。当前线程通过helpTransfer进行辅助扩容,扩容完成之后再进行插入操作
    4. 如果hash冲突了,且槽里面第一个元素的hash不为-1,需要对槽里面首元素进行加锁,然后将元素加进去
  4. 通过addCount()方法进行扩容

为了加深我们对于putVal()的理解,我们需要进一步分析该方法内部调用的几个关键方法

  1. spread()
  2. initTable()
  3. tabAt()
  4. casTabAt()
  5. helpTransfer()
  6. addCount()

1. spread()

static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash

static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}

这个方法将hash的高位扩展到低位,同时强制最高位为0(可能是为了让hash值保持为非负数吧)。该方法做了下面几件事情

  1. 我们将原int叫做A
  2. 这儿将原int右移16位得到的int叫做B,则B的高16位则全为0
  3. A和B异或,得到C,C的高位和A的高位相同
  4. C再和HASH_BITS按位与,得到结果D,D的符号位为0

2. initTable()

/**
 * Initializes table, using the size recorded in sizeCtl.
 */
private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    // table为null或者table长度为0,才进行初始化
    while ((tab = table) == null || tab.length == 0) {
        // sizeCtl为负数,说明有其他线程正在初始化table,当前线程让出cpu时间片,等待其他线程初始化完成
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        // 通过CAS设置sizeCtl为-1成功之后,则说明当前线程抢占到了锁,需要进行真正的初始化操作
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                // 这儿还需要对table进行二次校验。
                // 因为CAS设置成功,有可能是由于table已经初始化完成了。
                // 如果是这种情况,当前线程不需要再次初始化
                if ((tab = table) == null || tab.length == 0) {
                    // 当前线程通过`new Node[]`的方式进行`table`初始化
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    // 这儿的`n - (n >>> 2);`写得非常牛逼。
                    // 右移1位,大小折半,右移2位,大小折1/4。
                    // 这儿原始int减去1/4,则为3/4,即:0.75*原int,
                    // 正好是HashMap里面默认的负载因子
                    // 至于为什么要用位移和减法,而不直接使用除法,楼主认为是基于性能的考虑,除法的性能比加减法和位移要低得多。
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc; // 扩容保护
            }
            break;
        }
    }
    return tab;
}

该方法初始化tabletable的size记录在sizeCtl变量中。该方法采用自旋+CAS的方式实现了乐观锁,相比于悲观锁,带来了非常大的性能提升。

3. tabAt()

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

该方法目的是取tab数组中,下标为i的元素。那么为啥要这样写呢? 在理解之前,我们先来看看取数组元素的方法。

首先,我们知道数组元素在内存里面是连续存放的;
其次,数组元素存放的是引用地址,而非具体的值;
最后,数组取元素的方式可以通过数组地址+数组元素偏移量的方式来获取。

((long)i << ASHIFT) + ABASE这个表达式中,(long)i << ASHIFT表示偏移量,ABASE表示数组地址。

另外为啥又要使用getObjectVolatile()呢,是因为在并发的情况下,我们需要保证数组元素的内存可见性。

4. casTabAt()

static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                    Node<K,V> c, Node<K,V> v) {
    return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

关键的地方和tabAt()相同,不同的在于其功能。该方法用于对数组指定下标的元素进行CAS赋值。

5. 扩容方法

helpTransfer()addCount()涉及到扩容相关的知识点,本章暂且不表,后续会单独开一节来详细说明。

总结

至此我们分析了putVal()整体的结构。不过对于扩容辅助扩容相关知识点,我们并没有分析,而是一笔带过。原因在于扩容的知识相当得复杂,即使单开一节来分析都稍显不够。为了保证本节的简单些和清晰性,我们留待后续来说明。

即使只是分析了putVal()的皮毛,我们也能从中看到Doug Lea对于细节和性能处理方式的周全思虑,楼主阅读之后可谓是受益匪浅~

参考链接

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