HashMap

HashMap为什么线程不安全

HashMap的线程不安全体现在会造成死循环、数据丢失、数据覆盖等问题

其中死循环和数据丢失是在JDK1.7中出现的问题,在JDK1.8中已经得到解决,但是1.8中仍会有数据覆盖这样的问题。HashMap是线程不安全的,线程安全场景应该使用ConcurrentHashMap。

HashMap的数据丢失和死循环

HsshMap在JDK1.8(不含)之前对于新增元素的hash冲突的链表插入采用的是头插法,1.8之后开始改用尾插法
JDK1.7中HashMap的数据结构为数组和链表构成
在这里插入图片描述
HashMap添加元素通过哈希函数计算出插入的位置,因为哈希本身就存在概率性,所以极端情况下会有多个元素hash到一个值上,就会形成链表,采用头插法会修改插入的顺序,后插入的在前面,先插入的在后面。因为写这个代码的作者认为后来的值被查找的可能性更大一点,提升查找的效率。
多个线程同时往HashMap添加新元素时,多次resize会有一定概率会使HashMap出现环形链表,因为每次resize需要把旧的数据映射到新的哈希表,

HashMap-jdk1.7.0_80

向HashMap里面添加元素

public V put(K key, V value) {
    ...
    // 添加新元素
    addEntry(hash, key, value, i);
    return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
    // 如果当前map大小超出阈值,并且当前值再次触发了hash冲突,则resize map
    if ((size >= threshold) && (null != table[bucketIndex])) {
         //扩容
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

扩容

void resize(int newCapacity) {
    ...
    Entry[] newTable = new Entry[newCapacity];
    // 向新数组转移数据
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

转移数据至新数组

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    // 1. 遍历老table 转移数据
    for (Entry<K,V> e : table) {
        // 2. 如果元素不为空,遍历Entry元素 遍历链表
        while(null != e) {
            next = e.next;
            ...
            // 获取数据存取下标
            int i = indexFor(e.hash, newCapacity);
            // 头插法 hash冲突老数据存放在 新元素后方
            e.next = newTable[i];
            // 3. 然后 将 新元素 存入 数组
            newTable[i] = e;
            // 4. 继续遍历Entry子节点
            e = next;
        }
    }
}

单线程流程

在这里插入图片描述
假设我们有两个线程
线程一 仅创建了新的数组 还没有转移数据 后被挂起
线程二 已经完成全部流程

在这里插入图片描述
然后 线程一被调度回来执行
线程一开始转移数据
执行下面代码
三次循环分析

            // 假设线程一执行到这里就被调度挂起了,执行其他操作  此时 e = key3->key7->key5   所以next = key7
            // 二次循环      e = key7->key3 所以 next =key3
            // 三次循环      e = key3 所以next = null
            next = e.next;
            // 线程二执行完成后调度接着直接线程一
            // 线程二执行后 key7后面指向了key3   所以next = key7 ->key3 此时原来的元素key5 已丢失
            ...
            // 获取转移数据的存取下标
            int i = indexFor(e.hash, newCapacity);
            // 头插法 单线程下 此时应是 将数组之前先存放hash冲突下的数据 放在转移元素的后面,
            //继续执行一次循环  此时newTable[i] = null    所以 e = key3 -> null 
  			//二次循环    newTable[i] = key3    所以  e = key7->key3
  			//三次循环    newTable[i] = key7->key3  所以 e = key3->key7->key3  产生闭环
            e.next = newTable[i];
            //继续执行1 将 e 赋给  newtTable[i] = key3->null
            //二次循环   newTable[i] = key7->key3 
            //三次循环   newTable[i] =  key3->key7->key3 
            newTable[i] = e;
            //继续执行1  e = key7 -> key3
            //二次循环   e =  key3
            //三次循环技术 e = null 结束循环 产生环形链表
            e = next;

第一次循环 后 结果为
在这里插入图片描述
第二次循环后结果为
在这里插入图片描述
第三次循环后结果为
在这里插入图片描述
在以后对该HashMap进行操作时会出现死循环 Infinite Loop。

HashMap的数据覆盖

JDK1.7出现的问题,在JDK1.8中已经得到了很好的解决,JDK1.8直接在resize函数中完成了数据迁移。在进行元素插入时使用的是尾插法然后在扩容。

但是在1.8中仍会有数据覆盖这样的问题,先看put源码:

HashMap-jdk1.8
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null) //判断是否出现hash碰撞,如果没有hash碰撞则直接插入元素,此处线程不安全
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold) //++size此处线程不安全
            resize();
        afterNodeInsertion(evict);
        return null;
    }

其中代码if ((p = tab[i = (n - 1) & hash]) == null) 是判断是否出现hash碰撞:
比如两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所以此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

还有一种情况就是代码 if (++size > threshold)中的++size
同样还是线程A、B,这两个线程同时进行put操作时,假设当前HashMap的zise大小为10,当线程A执行到此行代码时,从主内存中获得size的值为10后准备进行+1操作,但是由于时间片耗尽只好让出CPU,线程B快乐的拿到CPU还是从主内存中拿到size的值10进行+1操作,完成了put操作并将size=11写回主内存,然后线程A再次拿到CPU并继续执行(此时size的值仍为10),当执行完put操作后,还是将size=11写回内存,此时线程A、B都执行了一次put操作,但是size的值只增加了1,所有说还是由于数据覆盖又导致了线程不安全。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值