后端---java中hashmap多线程并发问题详解

HashMap多线程并发问题分析

hashmap并发问题的症状:

hashmap多线程操作同时调用put()方法后可能导致get()死循环,从而使CPU使用率达到100%,从而使服务器宕机.

Java的API文档说HashMap是非线程安全的,应该用ConcurrentHashMap。但是在这里我们可以来研究一下原因。简单代码如下:

package com.king.hashmap;

import java.util.HashMap;

public class TestLock {

    private HashMap map = new HashMap();

    public TestLock() {
        Thread t1 = new Thread() {
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    map.put(new Integer(i), i);
                }
                System.out.println("t1 over");
            }
        };

        Thread t2 = new Thread() {
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    map.put(new Integer(i), i);
                }

                System.out.println("t2 over");
            }
        };

        Thread t3 = new Thread() {
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    map.put(new Integer(i), i);
                }

                System.out.println("t3 over");
            }
        };

        Thread t4 = new Thread() {
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    map.put(new Integer(i), i);
                }

                System.out.println("t4 over");
            }
        };
        t1.start();
        t2.start();
        t3.start();
        t4.start();
 public static void main(String[] args) {
        new TestLock();
    }
}

 我们来看这段代码会怎么执行.这段代码一共启了4个线程,每个线程执行map.put()方法往一个map同步的放入50000个数,当我们反复运行这个代码的时候,会出现线程t1,t2被hang住的情况,多数情况下是一个线程被hang住另一个成功结束,偶尔会10个线程都被hang住.

产生这个死循环的根源在于多个线程同时对一个未保护的共享变量---------"HashMap"数据结构的操作,当在所有操作的方法上加了"synchronized"后,一切恢复了正常。这时候我们可以采取使用"ConcurrentHashMap"来完成多线程实现HashMap的操作.

 ********************************************************************************************************************************************** 

                                                                        HashMap底层多线程不安全的原因

CPU利用率过高一般是因为出现了出现了死循环,导致部分线程一直运行,占用cpu时间。问题根本原因就是HashMap是非线程安全的,多个线程put的时候造成了某个key值Entry key List的死循环,然后再调用put方法操作的时候就会进入链表的死循环内。 

死循环how to be created?

在多线程访问的时候,由于其内部实现机制(在多线程环境且未作同步的情况下,对同一个HashMap做put操作可能导致两个或以上线程同时做rehash动作,就可能导致循环键表出现.

注意:不合理使用HashMap导致出现的是死循环而不是死锁。 

 <关于hashMap底层源代码实现感到疑惑的可参考我上篇博客

算法---hash算法原理(java中HashMap底层实现原理和源码解析)

https://blog.csdn.net/weixin_42504145/article/details/84309397 >

 **********************************************************************************************************************************************

(1)正常的ReHash过程(hashmap产生死循环链表的操作)

抄了个图做个演示。

  1. 我假设了我们的hash算法就是简单的用key mod 一下表的大小(也就是数组的长度)。
  2. 最上面的是old hash 表,其中的Hash表的size=2, 所以key = 3, 7, 5,在mod 2以后都冲突在table1这里了。
  3. 接下来的三个步骤是Hash表 resize成4,然后所有的 重新rehash的过程。

 

 

(2)并发的Rehash过程

(1)假设我们有两个线程。我用红色和浅蓝色标注了一下。我们再回头看一下我们的 transfer代码中的这个细节:

do {
    Entry next = e.next; // <--假设线程一执行到这里就被调度挂起了
    int i = indexFor(e.hash, newCapacity);
    e.next = newTable[i];
    newTable[i] = e;
    e = next;
} while (e != null);

 而我们的线程二执行完成了。于是我们有下面的这个样子。

 

注意:因为Thread1的 e 指向了key(3),而next指向了key(7),其在线程二rehash后,指向了线程二重组后的链表。我们可以看到链表的顺序被反转后。

(2)线程一被调度回来执行。

  1. 先是执行 newTalbe[i] = e。
  2. 然后是e = next,导致了e指向了key(7)。
  3. 而下一次循环的next = e.next导致了next指向了key(3)。

 (3)再接下来

 线程一接着工作。把key(7)摘下来,放到newTable[i]的第一个,然后把e和next往下移。

 (4)环形链接出现

e.next = newTable[i] 导致 key(3).next 指向了 key(7)。注意:此时的key(7).next 已经指向了key(3), 环形链表就这样出现了。 

 于是,当我们的线程一调用到,HashTable.get(11)时,悲剧就出现了——Infinite Loop

 

三种解决方案


Hashtable替换HashMap

Hashtable 是同步的,但由迭代器返回的 Iterator 和由所有 Hashtable 的“collection 视图方法”返回的 Collection 的 listIterator 方法都是快速失败的:在创建 Iterator 之后,如果从结构上对 Hashtable 进行修改,除非通过 Iterator 自身的移除或添加方法,否则在任何时间以任何方式对其进行修改,Iterator 都将抛出 ConcurrentModificationException。因此,面对并发的修改,Iterator 很快就会完全失败,而不冒在将来某个不确定的时间发生任意不确定行为的风险。由 Hashtable 的键和值方法返回的 Enumeration 不是快速失败的。

注意,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误做法:迭代器的快速失败行为应该仅用于检测程序错误。

Collections.synchronizedMap将HashMap包装起来

返回由指定映射支持的同步(线程安全的)映射。为了保证按顺序访问,必须通过返回的映射完成对底层映射的所有访问。在返回的映射或其任意 collection 视图上进行迭代时,强制用户手工在返回的映射上进行同步:

Map m = Collections.synchronizedMap(new HashMap());
...
Set s = m.keySet();  // Needn't be in synchronized block
...
synchronized(m) {  // Synchronizing on m, not s!
Iterator i = s.iterator(); // Must be in synchronized block
    while (i.hasNext())
        foo(i.next());
}

不遵从此建议将导致无法确定的行为。如果指定映射是可序列化的,则返回的映射也将是可序列化的。

ConcurrentHashMap替换HashMap

支持检索的完全并发和更新的所期望可调整并发的哈希表。此类遵守与 Hashtable 相同的功能规范,并且包括对应于 Hashtable 的每个方法的方法版本。不过,尽管所有操作都是线程安全的,但检索操作不必锁定,并且不支持以某种防止所有访问的方式锁定整个表。此类可以通过程序完全与 Hashtable 进行互操作,这取决于其线程安全,而与其同步细节无关。
检索操作(包括 get)通常不会受阻塞,因此,可能与更新操作交迭(包括 put 和 remove)。检索会影响最近完成的更新操作的结果。对于一些聚合操作,比如 putAll 和 clear,并发检索可能只影响某些条目的插入和移除。类似地,在创建迭代器/枚举时或自此之后,Iterators 和 Enumerations 返回在某一时间点上影响哈希表状态的元素。它们不会抛出 ConcurrentModificationException。不过,迭代器被设计成每次仅由一个线程使用。

 

 

你可能感兴趣的:(后端java,HashMap线程不安全,多线程HashMap,多线程并发问题,hashmap并发问题,解决hashmap宕机的方法)