虽然一直都知道HashMap是非线程安全的,但是直到真的踏入了坑才能有深刻认识。
locks[(int) (id % LOCK_NUM)].lock();//ReentrantLock.lock();
rowData = dataBase.get(id); //HashMap.get(key);
if (rowData == null) {
rowData = new RowData();
dataBase.put(id, rowData);
}
locks[(int) (id % LOCK_NUM)].unlock();
调试了一两个小时,主要是因为一直认为是ReentrantLock造成的死锁。正纳闷完全不符合死锁条件,怎么就会死锁。关键是这个死锁大概以30%的概率出现,^_^,感觉好幸运,要是3%的概率出现我就歇菜了。逗逼地F8了好久,直到记起来用JConsole查看死锁状态,发现结果如下。
Name: Thread-0
State: WAITING on java.util.concurrent.locks.ReentrantLock$NonfairSync@4097d66a owned by: Thread-2
Total blocked: 0 Total waited: 12
Name: Thread-1
State: WAITING on java.util.concurrent.locks.ReentrantLock$NonfairSync@4097d66a owned by: Thread-2
Total blocked: 0 Total waited: 6
Name: Thread-2
State: RUNNABLE
Total blocked: 1 Total waited: 12
Stack trace:
java.util.HashMap.getEntry(HashMap.java:465)
java.util.HashMap.get(HashMap.java:417)
有了线程栈,问题就很easy了,直接去HashMap.java看下发现问题 :为什么会产生循环链表参见这篇文章《HashMap死锁分析》
补充:自己看上面的这篇文章觉得篇幅略长。决定自己来写点篇幅简单的。
for (Entry e : table) {
while(null != e) {
Entry next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
假设有hash表hash[0] = e(3,7,null);//e表示链表;
线程1和线程2同时resize。
线程1刚执行完Entry
所以next = 7, e = 3;
线程2完成了rehash,所以当前链表尾hash[0] = e(7,3,null);//第一次逆转;
剩下都是线程1:
e.next = newTable[i] ;newTable[i] = e; //hash[0] = e(3,7,3); 循环链表。
简而言之就是如果Thread-1和Thread-2同时push并且需要Resize当前HashMap,由于并发它们不知道对方也在Resize,而Reasize过程中会对每一个Entry进行逆转,相当于两个Thread同时逆转一个单链表。所以在并发时如果不能保证Push操作的原子性,应该使用ConcunrentHashMap 。
final Entry getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry e = table[indexFor(hash, table.length)];
e != null;
e = e.next) { //465 多线程push时使e成为了循环链表,get一个不在其中的key会导致死循环
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}