HashMap modCount fast-fail 非原子性论证

技术博客已迁移至个人页,欢迎查看 yloopdaed.icu

您也可以关注 JPP - 这是一个Java养成计划,需要您的加入。


前言

HashMap源码中定义的成员变量并不多,其中我们最不熟悉的应该就是modCount,那么它到底是做什么的呢?

modCount

如果你没时间思考这篇文章,你可以直接跳转到 9.结论

modCount

modCount在HashMap中记录的是HashMap对象被修改的次数,这里专业的说法是集合在结构上修改时被会记录在modCount中。

文中源码版本为 JDK1.7,modCount的部分在JDK1.8中作用是相同的。只因为JDK1.7中源码比较简洁,所以本文选用JDK1.7来缩减篇幅。

在源码中记录到的modCount++的方法包括:

  • HashMap put方法[图片上传中...(modcount.jpg-dff60f-1604249139377-0)]

  • HashMap的remove->removeEntryForKey方法 通过key移除元素

  • HashMap的removeMapping方法,通过object移除元素

  • HashMap的clear方法

从这里可以看出,结构上的修改主要是添加和删除两部分。

线程不安全

我们都知道在JDK1.7中HashMap是线程不安全的,这个 不安全 我是分两方面理解的:

1 多线程数组扩容时出现循环链表问题

因为扩容时链表顺序会反转,所以多线程操作时可能会出现循环链表的情况,那么在get方法时就会死循环

JDK1.8中也修复了这个问题

2 多线程读写时造成数据混乱的问题

HashMap中有引入了一个 fast-fail 的概念,目的是避免高并发读写造成的数据错乱的隐患。

expectedModCount

expectedModCount这个变量被记录在HashIterator迭代器中。顾名思义,表示期望的修改次数,当期望修改的次数不等于实际修改的次数时,就会触发 fast-fail 快速失败的容错处理

fast-fail

final Entry<K,V> nextEntry() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    ...
}

迭代器调用 next() 方法时会调用 nextEntry() 方法,方法中首先会判断 modCount 与 expectedModCount 是否相等

如果不相等直接抛出 java.util.ConcurrentModificationException 异常

GeeksForGeeks中的解释为:

In multi threaded environment, if during the detection of the resource, any method finds that there is a concurrent modification of that object which is not permissible, then this ConcurrentModificationException might be thrown.

  1. If this exception is detected, then the results of the iteration are undefined.
  2. Generally, some iterator implementations choose to throw this exception as soon as it is encountered, called fail-fast iterators.

For example: If we are trying to modify any collection in the code using a thread, but some another thread is already using that collection, then this will not be allowed.

在多线程环境中,如果在检测资源期间,任何方法发现该对象存在并发修改,而这是不允许的,则可能会抛出此ConcurrentModificationException。

1 如果检测到此异常,则迭代结果不确定。

2 通常,某些迭代器实现选择将遇到此异常的异常立即抛出,称为快速失败迭代器。

例如:如果我们试图使用一个线程来修改代码中的任何集合,但是另一个线程已经在使用该集合,则将不允许这样做。

验证

相关代码可以在 JPP/ConcurrentModificationExceptionDemo类中查看。

HashMap m = new HashMap();
for (int i = 0; i <100 ; i++) {
    m.put(String.valueOf(i), "value"+i);
}

new Thread(new Runnable() {
    @Override
    public void run() {
        Iterator iterator = m.keySet().iterator();
        while (iterator.hasNext()) {
            String next = (String) iterator.next();
            if (Integer.parseInt(next) % 2 == 0) {
                System.out.println("thread 1");
                iterator.remove();
            }
        }
    }
}).start();

new Thread(new Runnable() {
    @Override
    public void run() {
        Iterator iterator  = m.keySet().iterator();
        while (iterator.hasNext()) {
            String next = (String) iterator.next();
            System.out.println(m.get(next));
        }
    }
}).start();

这里第一个线程中的 System.out.println("thread 1"); 的作用是 触发数据和内存同步

这部分内容和寄存器的 缓存行 知识有关,如果不触发数据和内存同步,第二个线程无法正确获取modCount。

单线程错误案例

HashMap m = new HashMap();
m.put("key1", "value2");
m.put("key2", "value2");
for (String key: m.keySet()) {
    if (key.equals("key2")) {
        m.remove(key);
    }
}

这个代码块也有可能发生 fast-fail

我们来看一下上面代码块编译后的class文件

HashMap m = new HashMap();
m.put("key1", "value2");
m.put("key2", "value2");
Iterator i$ = m.keySet().iterator();
while(i$.hasNext()) {
    Object key = i$.next();
    if (key.equals("key2")) {
        m.remove(key);
    }
}

这么看应该就很容易理解了,而且这个错误也很容易发生。

在迭代器遍历的过程中,会将key值为“key2”的元素移除。移除时调用的HashMap的remove方法会对modCount值+1,但是这个方法并不会同步expectedModCount的值。所以在下一次迭代器调用i$.next();方法时,会发生异常。

expectedModCount // For fast-fail:在以下方法会同步modCount值

  • HashIterator的构造方法
  • HashIterator的remove方法

所以将上面移除元素的代码。替换为 i$.remove(); 就可以了。

思考

关于 i++ 计算不是原子性的怀疑:

HashMap源码记录modCount++这个计算方式在多线程操作时如果不能保证原子性,那么岂不是也有可能触发ConcurrentModificationException异常?

验证过程:
1 因为HashMap的put操作会进行modCount++
2 modCount声明时也没有指明volatile
那么多线程put是否会造成modCount的值不准确?

相关代码可以在 JPP/ConcurrentModificationExceptionDemo类中查看。

static void atomicTest() throws InterruptedException {

    HashMap m = new HashMap();

    new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                // System.out.println(i);
                m.put(i, String.valueOf(i).hashCode());
            }
        }
    }).start();

    new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 10000; i < 20000; i++) {
                // System.out.println(i);
                m.put(i, String.valueOf(i).hashCode());
            }
        }
    }).start();

    Thread.sleep(5000);
    Iterator iterator = m.keySet().iterator();
    iterator.next(); // 对比modCount
}

运行的结果是,如果循环次数不多,最后可以保证modCount的数值正确。但是提升循环插入的次数,会锁住一个线程,导致其他线程的数据没有插入成功,但是modCount的值依然是正确的。

具体这个魂循环次数设定的阈值,我也没有过多尝试。至少目前我没有因为++计算不是原子性的原因出现过fast-fail

运行结果有意外收获:

modcount++.jpg

从上图可以看出,不仅在多线程写入的时候modCount的值无法保证(从expectedModCount看出),而且HashMap的size也不满足期望(因为多线程put时,两个线程的key不重复)

为了再次证明我的猜测,可以在多线程中添加 System.out.println(i); 代码,来达到内存同步的目的

结果不出所料:

sysmodcount++.jpg

结论

1 HashMap多线程读写时可能会抛出ConcurrentModificationException异常,这是fast-fail快速失败机制。

2 fast-fail实现的原理是判断modCount和expectedModCount是否相等

3 modCount++在多线程操作时无法保证原子性,甚至HashMap整个put方法都出现了问题

PS:所以在JDK1.7的ConcurrentHashMap中出现大量 UNSAFEvolatile 关键字。

最后

上文所有代码片段都是基于JDK1.7,虽然JDK1.8中对HashMap做了较大的改动。但是文章的思路和结论都是相同的。

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