一个验证HashMap在多线程环境下线程不安全的例子及dump分析

实例: 

package com.bijian.study.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");
			}
		};
		
		Thread t5 = new Thread() {
			public void run() {
				for (int i = 0; i < 50000; i++) {
					map.put(new Integer(i), i);
				}

				System.out.println("t5 over");
			}
		};
		
		t1.start();
		t2.start();
		t3.start();
		t4.start();
		t5.start();
	}

	public static void main(String[] args) {
		new TestLock();
	}
}

 

 运行结果:

1.  报如下错误

t5 over
t4 over
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 21604
at java.util.HashMap.addEntry(Unknown Source)
at java.util.HashMap.put(Unknown Source)
at com.bijian.study.hashmap.TestLock$1.run(TestLock.java:13)
t3 over
t2 over

2.这个程序会hang住不运行了,而且CPU会占用100%

 
下面我们对第二种错误进行分析

 

       JDK自带工具分析

1.       jconsole.exe(此工具未发现能分析Dump Thread的功能

点击运行

选中相应的进程,点“连接”即可进入Java监视和管理控制台,如下所示:

点击“线程”页签,查看线程情况

 

2.       jpsjstack <pid>

a.       cmd窗口,将JDK下的jps.exe拖入回车运行,获得相应线程的PID

这里TestLockPID5048

b.       然后在cmd中输入jstack 5048,回车,就可看到Dump thread信息

 

3.       jvisualvm.exe

点击运行JDK下的jvisualvm.exe

启动后,点击“线程Dump(T)”选项,即可看到Dump thread信息

  

Dump thread会看到,程序hang到:

"DestroyJavaVM" prio=6 tid=0x011ffc00 nid=0x11e0 waiting on condition [0x00000000]
   java.lang.Thread.State: RUNNABLE
"Thread-3" prio=6 tid=0x03f46c00 nid=0x1520 runnable [0x041cf000]
   java.lang.Thread.State: RUNNABLE
        at java.util.HashMap.transfer(Unknown Source)
        at java.util.HashMap.resize(Unknown Source)
        at java.util.HashMap.addEntry(Unknown Source)
        at java.util.HashMap.put(Unknown Source)
        at com.bijian.study.hashmap.TestLock$4.run(TestLock.java:42)
"Low Memory Detector" daemon prio=6 tid=0x01a93800 nid=0xac runnable [0x00000000]
   java.lang.Thread.State: RUNNABLE

      CPU利用率过高一般是因为出现了出现了死循环,导致部分线程一直运行,占用cpu时间。问题原因就是HashMap是非线程安全的,多个线程put的时候造成了某个keyEntry key List的死循环,问题就这么产生了。

当另外一个线程get 这个Entry List 死循环的key的时候,这个get也会一直执行。最后结果是越来越多的线程死循环,最后导致服务器dang掉。我们一般认为HashMap重复插入某个值的时候,会覆盖之前的值,这个没错。但是对于多线程访问的时候,由于其内部实现机制(在多线程环境且未作同步的情况下,对同一个HashMap做put操作可能导致两个或以上线程同时做rehash动作,就可能导致循环键表出现,一旦出现线程将无法终止,持续占用CPU,导致CPU使用率居高不下),就可能出现安全问题了。

 

三种方法解决此问题

1.Hashtable替换HashMap

private Hashtable map = new Hashtable();

替换

private HashMap map = new HashMap();

说明:

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

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

 

2.Collections.synchronizedMapHashMap包装起来

private Map map = Collections.synchronizedMap(new HashMap());

替换

private HashMap map = new 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());

  }

       不遵从此建议将导致无法确定的行为。

如果指定映射是可序列化的,则返回的映射也将是可序列化的。

 

3.ConcurrentHashMap替换HashMap

private ConcurrentHashMap map = new ConcurrentHashMap();

替换

private HashMap map = new HashMap();

说明:

       支持检索的完全并发和更新的所期望可调整并发的哈希表。此类遵守与 Hashtable 相同的功能规范,并且包括对应于 Hashtable 的每个方法的方法版本。不过,尽管所有操作都是线程安全的,但检索操作必锁定,并且支持以某种防止所有访问的方式锁定整个表。此类可以通过程序完全与 Hashtable 进行互操作,这取决于其线程安全,而与其同步细节无关。

检索操作(包括 get)通常不会受阻塞,因此,可能与更新操作交迭(包括 put remove)。检索会影响最近完成的更新操作的结果。对于一些聚合操作,比如 putAll clear,并发检索可能只影响某些条目的插入和移除。类似地,在创建迭代器/枚举时或自此之后,Iterators Enumerations 返回在某一时间点上影响哈希表状态的元素。它们不会抛出 ConcurrentModificationException。不过,迭代器被设计成每次仅由一个线程使用。

展开阅读全文

没有更多推荐了,返回首页