【小白必看-2】HashMap为什么不是线程安全的

HashMap底层是一个Entry数组,当发生hash冲突的时候,hashmap是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。

javadoc中关于hashmap的一段描述如下:

此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须 保持外部同步。(结构上的修改是指添加或删除一个或多个映射关系的任何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问,如下所示:

Map m = Collections.synchronizedMap(new HashMap());

1、在hashmap做put操作的时候会调用到以下的方法。

void addEntry(int hash, K key, V value, int bucketIndex) {
	Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);
    }

现在假如A线程和B线程同时对同一个数组位置调用addEntry,两个线程会同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失。

2、删除键值对的代码如下:

final Entry<K,V> removeEntryForKey(Object key) {
        int hash = (key == null) ? 0 : hash(key.hashCode());
        int i = indexFor(hash, table.length);
        Entry<K,V> prev = table[i];
        Entry<K,V> e = prev;
 
        while (e != null) {
            Entry<K,V> next = e.next;
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }
 
        return e;
    }

多个线程同时操作同一个数组位置的时候,也都会先取得现在状态下该位置存储的头结点,然后各自去进行计算操作,之后再把结果写会到该数组位置去,其实写回的时候可能其他的线程已经就把这个位置给修改过了,就会覆盖其他线程的修改

3、addEntry中当加入新的键值对后,键值对总数量超过门限值的时候会调用一个resize操作,

这个操作会新生成一个新的容量的数组,然后对原数组的所有键值对重新进行计算和写入新的数组,之后指向新生成的数组。代码如下:

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
 
        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }

当多个线程同时检测到总数量超过门限值的时候就会同时调用resize操作,各自生成新的数组并rehash后赋给该map底层的数组table,结果最终只有最后一个线程生成的新数组被赋给table变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的table作为原始数组,这样也会有问题

一、扩容过程

我们知道,HashMap在扩容的时候,是通过重新创建一个新的hash表,把原来旧数组中的Entry一个个迁移到新数组的,这个过程的实现方法如下

void transfer(Entry[] newTable) {
        Entry[] src = table;
        int newCapacity = newTable.length;
 
        //将每条链中的元素迁移过去
        for (int j = 0; j < src.length; j++) {
            Entry<K,V> e = src[j];
            if (e != null) {
                src[j] = null;   
            //遍历第j条链
                do {
                    Entry<K,V> next = e.next;
 
                    // 计算在newTable中的位置,原来在同一条链上的元素可能被分配到不同的位置
                    int i = indexFor(e.hash, newCapacity);   
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
    }

该方法中最核心的便是链的重新生成,代码如下

Entry<K,V> next = e.next;

// 计算在newTable中的位置,原来在同一条链上的元素可能被分配到不同的位置
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;

举个例子,现在有一条链,里面有四个元素,分别是在这里插入图片描述
经过计算,它们中e1,e2,e4要迁移到newTable[2]中,初始状态为

e指向e1,next指向e2

1.迁移e1

由于一开始newTable[2]里没有元素,所以newTable[2]为null

e.next=newTable[2];将e1连接到了null

newTable[2]=e; 此时新数组中链头放了e1

e=next;指针后移,状态如下
在这里插入图片描述

2.迁移e2

e.next=newTable[2];将e2的连接到了e1

newTable[2]=e; 此时新数组中链头放了e2

e=next;指针继续后移,状态如下
在这里插入图片描述

3.迁移e3

e3会迁移到另一个桶里,原理跟迁移e1是一样的

4.迁移e4

同理e2

最后迁移完的链为 e4—>e2—>e1

newTable[2]桶里装着e4

小总结:其实就是对着原链表,利用一个桶位进行重链(把新进来的元素连接上链头元素,并占用这个链头)

二、多线程下出现的问题

假设有两个线程,1和2

线程1执行到第一步,也就是这个状态,就被挂起了
在这里插入图片描述
然而线程2顺利的完成了该子链的迁移,如下

在这里插入图片描述
此时我们发现,线程1的e和next如上,还是指向在e1和e2,然而在内存中整条链的顺序都变了

在这时,线程1又继续工作,按照指令

迁移第1次:

e.next=newTable[2]; 此时的链头已经由线程2改为e4了,所以e1会连上e4

newTable[2]=e;

e=next; 这里e1又指向e2

next=e.next;变为

在这里插入图片描述
迁移第2次:

将e2连接上链头e1,e2作为链头,变为

在这里插入图片描述
迁移第3次:

将e1连接上链头e2,e1作为链头,e指向next,next指向e的下一个,变为
在这里插入图片描述
我们会发现迁移来迁移去都无法跳出这个环形链表,所以会发生扩容错误

以上是我个人的理解,如果读者发现有误还请不吝提醒

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

和你在一起^_^

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值