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

本文介绍了一个HashMap并发修改引发的问题案例,分析了问题产生的原因,并提供了三种解决方案:使用Hashtable、Collections.synchronizedMap包装HashMap以及使用ConcurrentHashMap。

转自:http://bijian1013.iteye.com/blog/1876078

实例: 

Java代码   收藏代码
  1. package com.bijian.study.hashmap;  
  2.   
  3. import java.util.HashMap;  
  4.   
  5. public class TestLock {  
  6.   
  7.     private HashMap map = new HashMap();  
  8.   
  9.     public TestLock() {  
  10.         Thread t1 = new Thread() {  
  11.             public void run() {  
  12.                 for (int i = 0; i < 50000; i++) {  
  13.                     map.put(new Integer(i), i);  
  14.                 }  
  15.                 System.out.println("t1 over");  
  16.             }  
  17.         };  
  18.   
  19.         Thread t2 = new Thread() {  
  20.             public void run() {  
  21.                 for (int i = 0; i < 50000; i++) {  
  22.                     map.put(new Integer(i), i);  
  23.                 }  
  24.   
  25.                 System.out.println("t2 over");  
  26.             }  
  27.         };  
  28.           
  29.         Thread t3 = new Thread() {  
  30.             public void run() {  
  31.                 for (int i = 0; i < 50000; i++) {  
  32.                     map.put(new Integer(i), i);  
  33.                 }  
  34.   
  35.                 System.out.println("t3 over");  
  36.             }  
  37.         };  
  38.           
  39.         Thread t4 = new Thread() {  
  40.             public void run() {  
  41.                 for (int i = 0; i < 50000; i++) {  
  42.                     map.put(new Integer(i), i);  
  43.                 }  
  44.   
  45.                 System.out.println("t4 over");  
  46.             }  
  47.         };  
  48.           
  49.         Thread t5 = new Thread() {  
  50.             public void run() {  
  51.                 for (int i = 0; i < 50000; i++) {  
  52.                     map.put(new Integer(i), i);  
  53.                 }  
  54.   
  55.                 System.out.println("t5 over");  
  56.             }  
  57.         };  
  58.           
  59.         t1.start();  
  60.         t2.start();  
  61.         t3.start();  
  62.         t4.start();  
  63.         t5.start();  
  64.     }  
  65.   
  66.     public static void main(String[] args) {  
  67.         new TestLock();  
  68.     }  
  69. }  

 

 运行结果:

1.  报如下错误

Text代码   收藏代码
  1. t5 over  
  2. t4 over  
  3. Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 21604  
  4. at java.util.HashMap.addEntry(Unknown Source)  
  5. at java.util.HashMap.put(Unknown Source)  
  6. at com.bijian.study.hashmap.TestLock$1.run(TestLock.java:13)  
  7. t3 over  
  8. t2 over  

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

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

 

       JDK自带工具分析

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

点击运行

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

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

 

2.       jps、jstack <pid>

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

这里TestLock的PID是5048

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

 

3.       jvisualvm.exe

点击运行JDK下的jvisualvm.exe。

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

  

Dump thread会看到,程序hang到:

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

      CPU利用率过高一般是因为出现了出现了死循环,导致部分线程一直运行,占用cpu时间。问题原因就是HashMap是非线程安全的,多个线程put的时候造成了某个key值Entry 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.synchronizedMap将HashMap包装起来

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。不过,迭代器被设计成每次仅由一个线程使用。

转自:http://bijian1013.iteye.com/blog/1876078

<think> 我们正在讨论Java线安全,用户现在想了解在实际项目中识别和修复Java线安全问题的方法。根据之前的对话,我们已经详细讲解了线安全的概念、根源和解决方法。现在需要结合实践,给出具体的方法步骤和示例。 参考之前的引用: 引用[1]提到了死锁避免,虽然直接相关,但属于线安全问题的一部分,可以侧面说明问题复杂性。 引用[2]提到了线控制方法(wait, yield, join),这些在修复线安全问题时可能会用到。 用户需求:实际项目中识别和修复Java线安全问题的方法 思路: 1. 识别线安全问题:常见场景、工具辅助 2. 修复方法:根据问题类型选择同步机制 3. 实战示例:展示问题代码和修复后的代码 4. 最佳实践和注意事项 注意:使用数学公式时,按照要求用$...$或$$...$$格式。由于线安全涉及计数(如计数器),可以用数学表达式说明问题(如两个线同时递增导致丢失更新:$count = count + 1$操作的非原子性)。 </think> ### 实际项目中识别和修复Java线安全问题的方法 #### 1. **识别线安全问题** 在实际项目中,可通过以下方式识别线安全问题: - **代码审查关键点**: - **共享状态访问**:检查多线程共享的变量(特别是非final字段) - **复合操作**:查找"检查后执行"模式(如`if(!map.containsKey(k)) map.put(k,v)`) - **非线安全类**:识别`ArrayList`/`HashMap`等未同步集合的使用 - **静态变量修改**:验证全局状态的并发修改路径 - **诊断工具**: - **JConsole/VisualVM**:监控线阻塞和死锁情况[^1] - **Thread Dump分析**:使用`jstack`捕获线状态,检测锁竞争 - **静态分析工具**:FindBugs/SpotBugs的"MT_CORRECTNESS"规则集 - **压力测试工具**:JMeter模拟高并发场景暴露问题 **典型问题模式示例**: ```java // 问题1:非原子操作 private int counter = 0; public void increment() { counter++; // 非原子操作,需同步 } // 问题2:非线安全集合 List<String> dataList = new ArrayList<>(); public void addData(String item) { dataList.add(item); // 多个线同时add可能破坏内部结构 } ``` #### 2. **修复线安全问题** 基于问题类型采取针对性修复策略: - **原子性问题修复**: ```java // 修复方案1:synchronized(适合方法级控制) private final Object lock = new Object(); public void safeIncrement() { synchronized(lock) { counter++; } } // 修复方案2:Atomic原子类(更高性能) private AtomicInteger atomicCounter = new AtomicInteger(0); public void atomicIncrement() { atomicCounter.incrementAndGet(); } ``` - **可见性问题修复**: ```java // volatile保证可见性(适合状态标志位) private volatile boolean shutdownRequested = false; public void shutdown() { shutdownRequested = true; // 所有线立即可见 } ``` - **集合类修复**: ```java // 替换为并发集合 Map<String, String> safeMap = new ConcurrentHashMap<>(); // 或使用Collections.synchronized包装 List<String> safeList = Collections.synchronizedList(new ArrayList<>()); ``` #### 3. **复合操作修复策略** 对于复杂操作,需保证整个逻辑原子性: ```java // 错误示例:"检查-写入"竞态条件 if (!map.containsKey(key)) { map.put(key, value); } // 修复方案1:ConcurrentHashMap原子方法 map.computeIfAbsent(key, k -> createValue(k)); // 修复方案2:显式同步锁 synchronized(lock) { if (!map.containsKey(key)) { map.put(key, value); } } ``` #### 4. **死锁预防实践** 参考引用[^1],避免死锁的关键原则: 1. **锁顺序一致性**:全局定义锁获取顺序(如按对象哈希排序) 2. **超时机制**:使用`tryLock(timeout)`替代无条件锁 3. **锁粒度控制**:避免嵌套锁,使用细粒度锁 4. **无锁编**:优先考虑`ConcurrentLinkedQueue`等无锁结构 #### 5. **验证与测试** 修复后必须验证: - **单元测试**:使用`CountDownLatch`/`CyclicBarrier`模拟并发 - **压力测试**:JMeter模拟1000+并发请求 - **持续监控**:生产环境启用`-XX:+PrintConcurrentLocks`监控锁竞争 #### 6. **最佳实践总结** 1. **优先使用可变对象**:如`String`/`LocalDateTime`天生线安全 2. **线封闭策略**:使用`ThreadLocal`存储线特定数据 3. **最小化同步范围**:同步块只包含必要代码 4. **并发库优先原则**:多用`java.util.concurrent`而非手动同步 5. **文档化线策略**:在代码注释中明确线安全保证级别[^1] > **经验法则**:当每秒并发请求$QPS > 1000$时,同步开销$T_{sync}$将成为性能瓶颈: > $$ > \text{理论吞吐量} = \frac{1}{T_{process} + T_{sync}} > $$ > 此时应优先考虑无锁数据结构或并发容器优化。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值