jones's technical blog

  • 首页
  • 文章归档
  • 默认分类
  • 关于页面

  • 搜索
博客系统 linux 酸酸乳

HashMap 在并发下造成的问题

发表于 2020-05-09 | 分类于 java 基础 | 0 | 阅读次数 184

jdk 1.8 以前, 并发下使用 HashMap 会造成死循环

以前我们写代码时因为一些原因使用了 HashMap, 当时我们还是用的单线程, 一切都 ok, 但是后来单线程无法满足我们的需求, 于是我们的程序变成了多线程. 变成了多线程后的程序发布到线上, 产生了 程序占 cpu 100% 的问题, 查看堆栈, 我们发现问题出在了 get() 操作上, 重启程序后, 问题消失, 但是没多久问题又发生了, 而且这个问题在测试环境很难复现.

我们看下代码就会发现, HashMap被多个线程操作, 而 HashMap 是线程不安全的, 应该用线程安全的 ConcurrentHashMap.

在 jdk 1.8 以前, 并发下使用 HashMap 会造成死循环, 因为并发下的 Rehash会使得元素间形成一个循环链表, 不过, jdk 1.8 之后, 解决了这个问题, 但是多线程下使用 HashMap还是会存在其他的问题, 比如数据丢失, 因为多线程put的时候,当索引相同而又同时达到链表的末尾时,另一个线程put的数据会把之前线程put的数据覆盖掉,就会产生数据丢失.

曾经有网友向 sun 建议 HashMap 支持并发下使用, 但是被 Oracle 拒绝了, 他们认为要在 并发下使用 HashMap 就用 java.util.concurrent 包下的 ConcurrentHashMap.

更多详情请查看: 疫苗:JAVA HASHMAP的死循环

HashMap 和 ConcurrentHashMap 的区别

  • HashMap 的设计目标就是简单高效, 没有采取任何的措施来保证 put remove 操作的多线程安全. 下面这张图是 jdk 1.8 的 HashMap 的 put 操作逻辑, 可以看出 put 操作的要么是整个散列表, 要么是某个hash 桶里的链表或者红黑树, 而这些过程都没有采取任何措施保证多线程的安全. 在这个复杂的逻辑中, 任何一个线程改动了散列表的结构, 都有可能造成另一个线程的操作失败.

图片来源自

  • ConcurrentHashMap 与 HashMap 相比,对多线程安全有了保证,为了这个多线程安全,ConcurrentHashMap 并没有采用 HashMap 的单散列表设计,而是用了 (jdk 1.8 以下采用了分段数组 + 链表实现, jdk 1.8 采用的数据结构和 HashMap 一样, 数组 + 链表 / 红黑二叉树), HashTable 和 jdk 1.8 以前的 HashMap 一样, 底层的数据结构都是采用的 数组 + 链表, 链表是主体, 链表是为了解决 hash 冲突而存在的.

    为什么不建议在并发下使用 HashTable

    HashTable 相比 ConcurrentHashMap, 对并发的处理就粗糙很多, 单纯的通过锁住整个散列表来保证多线程的安全性. 虽然在设计和实现上比 ConcurrentHashMap 简单许多, 但是在效率上远远不及 ConcurrentHashMap, 所以目前多线程环境下不建议使用 HashTable, 而是建议使用 ConcurrentHashMap

    为什么 jdk 1.8 后 避免了并发下 HashMap死循环的问题

    我们来看下 HashMap resize 的源码

     final Node<K,V>[] resize() {
            Node<K,V>[] oldTab = table;
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            int oldThr = threshold;
            int newCap, newThr = 0;
            if (oldCap > 0) {
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                }
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                         oldCap >= DEFAULT_INITIAL_CAPACITY)
                    newThr = oldThr << 1; // double threshold
            }
            else if (oldThr > 0) // initial capacity was placed in threshold
                newCap = oldThr;
            else {               // zero initial threshold signifies using defaults
                newCap = DEFAULT_INITIAL_CAPACITY;
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
            if (newThr == 0) {
                float ft = (float)newCap * loadFactor;
                newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                          (int)ft : Integer.MAX_VALUE);
            }
            threshold = newThr;
            @SuppressWarnings({"rawtypes","unchecked"})
                Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
            table = newTab;
            if (oldTab != null) {
                for (int j = 0; j < oldCap; ++j) {
                    Node<K,V> e;
                    if ((e = oldTab[j]) != null) {
                        oldTab[j] = null;
                        if (e.next == null)
                            newTab[e.hash & (newCap - 1)] = e;
                        else if (e instanceof TreeNode)
                            ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                        else { // preserve order
                            Node<K,V> loHead = null, loTail = null;
                            Node<K,V> hiHead = null, hiTail = null;
                            Node<K,V> next;
                            do {
                                next = e.next;
                                if ((e.hash & oldCap) == 0) {
                                    if (loTail == null)
                                        loHead = e;
                                    else
                                        loTail.next = e;
                                    loTail = e;
                                }
                                else {
                                    if (hiTail == null)
                                        hiHead = e;
                                    else
                                        hiTail.next = e;
                                    hiTail = e;
                                }
                            } while ((e = next) != null);
                            if (loTail != null) {
                                loTail.next = null;
                                newTab[j] = loHead;
                            }
                            if (hiTail != null) {
                                hiTail.next = null;
                                newTab[j + oldCap] = hiHead;
                            }
                        }
                    }
                }
            }
            return newTab;
        }
    

    我们重点看 39 行后的代码(仅考虑链表部分), 它声明了两队指针, 维护了两个链表, 在末端添加新的元素(在多线程情况下, 第二个线程重复第一个线程的操作). 这样就避免了死循环问题.

jones wechat
更多精彩内容请关注微信公众号
  • 本文作者: jones
  • 本文链接: https://www.lushuaiyu.com/archives/hashmap在并发下造成的问题
  • 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!
# 博客系统 # linux # 酸酸乳
Mybatis Plus 逆向工程
jdk 8 获取 昨天的最大和最小时间
  • 文章目录
  • 站点概览
jones

jones

程序猿

46 日志
16 分类
3 标签
Github E-mail
Creative Commons
0%
© 2021 jones
主题 - NexT.Pisces