前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >为什么 HashMap 是线程不安全的?

为什么 HashMap 是线程不安全的?

作者头像
代码宇宙
发布2023-02-23 10:12:07
3270
发布2023-02-23 10:12:07
举报
文章被收录于专栏:代码宇宙代码宇宙

前言

为什么说 HashMap 是线程不安全的,下面,一起学习一下吧。

先上一张解释线程安全的图

图中 Main 方法中有三个线程,三个线程共享 num 变量,故 num 变量是 static 修饰的,使用 static 修饰后,由于没有进行原子操作导致,线程 1 在判断完 num 大小后,时间片被分配到线程 2 ,线程 2 执行完毕后时间片会到线程 1 ,这个时候线程 1 就输出了错误的 num,这是一个很经典的线程安全问题。

再举一个复杂点的例子,HashMap,所有人知道 HashMap 是线程不安全的,但是恐怕没几个人到底为什么不安全,更没多少人能证明不安全。

关于 HashMap 线程不安全可以使用以下代码复现:

代码语言:javascript
复制
final Map<Integer, String> map = new HashMap<>();

final Integer targetKey = 0b1111_1111_1111_1111; // 65 535
final String targetValue = "v";
map.put(targetKey, targetValue);

new Thread(() -> {
    IntStream.range(0, targetKey).forEach(key -> map.put(key, "someValue"));
}).start();


while (true) {
    if (!targetValue.equals(map.get(targetKey))) {
        throw new RuntimeException("HashMap is not thread safe.");
    }
}

运行

代码语言:javascript
复制
Exception in thread "main" java.lang.RuntimeException: HashMap is not thread safe.
  at com.example.demo.DMYZDemo.main(DMYZDemo.java:26)

首先,第5行将 targetKey = targetValue 添加到了 map 中,按道理说这个 targetKey 如果不被删除,那么第13行的判断将会永不成立,但是为什么抛出了异常呢?

这里要说下,HashMap 的扩容过程

代码语言:javascript
复制
/**
     * Rehashes the contents of this map into a new array with a
     * larger capacity.  This method is called automatically when the
     * number of keys in this map reaches its threshold.
     *
     * If current capacity is MAXIMUM_CAPACITY, this method does not
     * resize the map, but sets threshold to Integer.MAX_VALUE.
     * This has the effect of preventing future calls.
     *
     * @param newCapacity the new capacity, MUST be a power of two;
     *        must be greater than current capacity unless current
     *        capacity is MAXIMUM_CAPACITY (in which case value
     *        is irrelevant).
     */
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {//最大容量为 1 << 30
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];//新建一个新表
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;//是否再hash
        transfer(newTable, rehash);//完成旧表到新表的转移
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

    /**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {//遍历同桶数组中的每一个桶
            while(null != e) {//顺序遍历某个桶的外挂链表
                Entry<K,V> next = e.next;//引用next
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);//找到新表的桶位置;原桶数组中的某个桶上的同一链表中的Entry此刻可能被分散到不同的桶中去了,有效的缓解了哈希冲突。
                e.next = newTable[i];//头插法插入新表中
                newTable[i] = e;
                e = next;
            }
        }
    }

对于resize的过程,相对来讲是比较简单清晰易于理解的。旧桶数组中的某个桶的外挂单链表是通过头插法插入新桶数组中的,并且原链表中的Entry结点并不一定仍然在新桶数组的同一链表。

这里很容易就想到多线程情况下,隐约感觉这个 transfer 方法在多线程环境下会乱套。事实上也是这样的,由于缺乏同步机制,当多个线程同时 resize 的时候,某个线程 t 所持有的引用 next(参考上面代码next指向原桶数组中某个桶外挂单链表的下一个需要转移的 Entry ),可能已经被转移到了新桶数组中,那么最后该线程t实际上在对新的桶数组进行 transfer 操作。

如果有更多的线程出现这种情况,那很可能出现大量线程都在对新桶数组进行transfer,那么就会出现多个线程对同一链表无限进行链表反转的操作,极易造成死循环,数据丢失等等,因此 HashMap 不是线程安全的,考虑在多线程环境下使用并发工具包下的 ConcurrentHashMap。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-03-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 代码宇宙 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档