博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
"通过jconsole(或者thread dump),可以看到线程停在了transfer方法的while循环处"
阅读量:6610 次
发布时间:2019-06-24

本文共 5141 字,大约阅读时间需要 17 分钟。

"通过jconsole(或者thread dump),可以看到线程停在了transfer方法的while循环处"

分析多线程并发写HashMap线程被hang住的原因

发表于 2010年08月07日 05:05 | Hits: 4367
Tag: |

在blogjava上看到一文,激发了我那能害死猫的好奇,所以很费劲的琢磨了这个问题。由于涉及的内容较多,就单独发文阐述一下。

文中提到的问题程序如下:

public class TestLock {  private final HashMap map = new HashMap();  public TestLock() {    final Thread t1 = new Thread() {      @Override      public void run() {        for(int i=0; i<500000; i++) {          map.put(new Integer(i), i);        }        System.out.println("t1 over");      }    };    final Thread t2 = new Thread() {      @Override      public void run() {        for(int i=0; i<500000; i++) {          map.put(new Integer(i), i);        }        System.out.println("t2 over");      }    };    t1.start();    t2.start();  }   public static void main(final String[] args) {    new TestLock();  }}

就是启了两个线程,不断的往一个非线程安全的HashMap中put内容,put的内容很简单,key和value都是从0自增的整数(这个put 的内容做的并不好,以致于后来干扰了我分析问题的思路)。对HashMap做并发写操作,我原以为只不过会产生脏数据的情况,但反复运行这个程序,会出现 线程t1、t2被hang住的情况,多数情况下是一个线程被hang住另一个成功结束,偶尔会两个线程都被hang住。说到这里,你如果觉得不好好学习 ConcurrentHashMap而在这瞎折腾就手下留情跳过吧。

好吧,分析下HashMap的put函数源码看看问题出在哪,这里就罗列出相关代码(jdk1.6):

public V put(final K key, final V value) {    if (key == null) {      return putForNullKey(value);    }    final int hash = hash(key.hashCode());    final int i = indexFor(hash, table.length);    for (Entry
e = table[i]; e != null; e = e.next) {//@标记1 Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { final V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } }  modCount++; addEntry(hash, key, value, i); return null; } void addEntry(final int hash, final K key, final V value, final int bucketIndex) { final Entry
e = table[bucketIndex]; table[bucketIndex] = new Entry
(hash, key, value, e); if (size++ >= threshold) { resize(2 * table.length); } } void resize(final int newCapacity) { final Entry[] oldTable = table; final int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; }  final Entry[] newTable = new Entry[newCapacity]; transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); } void transfer(final Entry[] newTable) { final Entry[] src = table; final int newCapacity = newTable.length; final long time1 = System.currentTimeMillis(); for (int j = 0; j < src.length; j++) { Entry
e = src[j]; if (e != null) { src[j] = null; int k=0;//@标记2 do { final Entry
next = e.next; final int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; if (k++ > 1000) {//@标记3 System.out.println(Thread.currentThread().getName()+ ",e==next:"+(e==e.next)+",e==next.next:"+(e==e.next.next)+ ",e:"+e+",next:"+e.next+",eq:"+e.equals(e.next)); try { Thread.sleep(2000); } catch (final Exception e2) { }  } } while (e != null); } } }

通过jconsole(或者thread dump),可以看到线程停在了transfer方法的while循环处。这个transfer方法的作用是,当Map中元素数超过阈值需要resize 时,它负责把原Map中的元素映射到新Map中。我修改了HashMap,加上了@标记2和@标记3的代码片断,以打印出死循环时的状态,结果死循环线程 总是出现类似这样的输出:“Thread- 1,e==next:false,e==next.next:true,e:108928=108928,next:108928=108928,eq:true”。

这个输出表明:
1)这个Entry链中的两个Entry之间的关系是:e=e.next.next,造成死循环。
2)e.equals(e.next),但e!=e.next。因为测试例子中两个线程put的内容一样,并发时可能同一个key被保存了多个value,这种错误是在addEntry函数产生的,但这和线程死循环没有关系。

接下来就分析transfer中那个while循环了。先所说这个循环正常的功能:src[j]保存的是映射成同一个hash值的多个Entry的 链表,这个src[j]可能为null,可能只有一个Entry,也可能由多个Entry链接起来。假设是多个Entry,原来的链是 (src[j]=a)->b(也就是src[j]=a,a.next=b,b.next=null),经过while处理后得到了 (newTable[i]=b)->a。也就是说,把链表的next关系反向了。

再看看这个while中可能在多线程情况下引起问题的语句。针对两个线程t1和t2,这里它们可能的产生问题的执行序列做些个人分析:

1)假设同一个Entry列表[e->f->...],t1先到,t2后到并都走到while中。t1执行“e.next = newTable[i];newTable[i] = e;”这使得e.next=null(初始的newTable[i]为null),newTable[i]指向了e。这时t2执行了“e.next = newTable[i];newTable[i] = e;”,这使得e.next=e,e死循环了。因为循环开始处的“final Entrynext = e.next;”,尽管e自己死循环了,在最后的“e = next;”后,两个线程都会跳过e继续执行下去。

2)在while中逐个遍历Entry链表中的Entry而把next关系反向时,newTable[i]成为了被交换的引用,可疑的语句在于 “e.next = newTable[i];”。假设链表e->f->g被t1处理成eg,造成了死循环。所以,理论上来说,死循环的Entry个数可能很多。 尽管产生了死循环,但是t1执行到了死循环的右边,所以是会继续执行下去的,而t2如果执行“final Entrynext = e.next;”的next为null,则也会继续执行下去,否则就进入了死循环。

3)似乎情况会更复杂,因为即便线程跳出了死循环,它下一次做resize进入transfer时,有可能因为之前的死循环Entry链表而被 hang住(似乎是一定会被hang住)。也有可能,在put检查Entry链表时(@标记1),因为Entry链表的死循环而被hang住。也似乎有可 能,活着的线程和死循环的线程同时执行在while里后,两个线程都能活着出去。所以,可能两个线程平安退出,可能一个线程hang在transfer 中,可能两个线程都被hang住而又不一定在一个地方。

4)我反复的测试,出现一个线程被hang住的情况最多,都是e=e.next.next造成的,这主要就是例子put两份增量数据造成的。我如果 去掉@标记3的输出,有时也能复现两个线程都被hang住的情况,但加上后就很难复现出来。我又把put的数据改了下,比如让两个线程put范围不同的数 据,就能复现出e=e.next,两个线程都被hang住的情况。

上面罗哩罗嗦了很多,一开始我简单的分析后觉得似乎明白了怎么回事,可现在仔细琢磨后似乎又不明白了许多。有一个细节是,每次死循环的key的大小 也是有据可循的,我就不打哈了。感觉,如果样本多些,可能出现问题的原因点会很多,也会更复杂,我姑且不再蛋疼下去。至于有人提到 ConcurrentHashMap也有这个问题,我觉得不大可能,因为它的put操作是加锁的,如果有这个问题就不叫线程安全的Map了。

原文链接:

转载地址:http://oaiso.baihongyu.com/

你可能感兴趣的文章
JavaScript的简单继承实现案例
查看>>
第六篇 VIM你值得拥有!
查看>>
项目管理学习笔记之八.课程总结
查看>>
setjmp与longjmp的分析
查看>>
generate ascii table
查看>>
2013吉林通化邀请赛 1005 GCD and LCM
查看>>
高淇java300集JAVA常用类作业
查看>>
<Linux命令行学习 第一节> CentOS在虚拟机的安装
查看>>
无Paper不论文
查看>>
mysql设置字符集CHARACTER SET
查看>>
redis 系列15 数据对象的(类型检查,内存回收,对象共享)和数据库切换
查看>>
log框架集成
查看>>
python命令行下安装redis客户端
查看>>
如何在Oracle中复制表结构和表数据
查看>>
[河南省ACM省赛-第四届] 序号互换 (nyoj 303)
查看>>
3 Oracle 32位客户端安装及arcgis连接
查看>>
[MFC] MFC编译程序,缺少MFC动态链接库的解决
查看>>
Sticker.js – 帮助你在网站中加入贴纸效果
查看>>
欧拉路与欧拉回路的性质
查看>>
iOS之UI--关于modal
查看>>