最近公司同僚消息隊列出現此種情況,以下分析
一、并發場景下HashMap.get導緻cpu耗光
Java中HashMap.get導緻cpu耗光bug,是由于HashMap.get裡面的for循環出現了無限循環,出現死循環是因為map中的桶連結清單出現了循環連結清單,循環連結清單是因為并發put操作造成的,同時進行了resize();如果隻有一個線程進行put操作,是不會出現循環連結清單,是以讀寫并發不會出現死循環,隻有寫寫并發才有可能出現死循環。
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
假設一個初始容量大小為2的HashMap(大多數人應該知道HashMap是采用數組的方式來存儲key,value對象,基于對key的hashcode的hash來決定存儲到數組的位置,沖突的情況下采用開放連結清單的方式來解決),目前裡面有兩個Entry,假設其存儲在數組的同一位置,存儲結構類似如下:
Entry[0] = A(.next) –> B(.next) –> null
假設同時有兩個線程調用了put,同時觸發resize。
第二個線程目前執行到Entry next = e.next這一行,此時其擷取到的e為A,next為B。
第一個線程已執行完transfer動作,但還沒執行完resize的剩餘動作,如A和B在容量擴大為4時hash到的位置仍然相同,那麼此時transfer中的newTable的結構變成:
Entry[0] = B(.next) –> A(.next) –> null
此時第二個線程繼續執行,進入這段代碼:
do {
// 此時e為A,e.next為B,此行相當于next = B;
Entry<K,V> next = e.next;
// 假設i=0
int i = indexFor(e.hash, newCapacity);
// newTable[0]為null,此行相當于A.next = null;
e.next = newTable[i];
// 此行相當于newTable[0] = A;
newTable[i] = e;
// 此行相當于e = B;
e = next;
} while (e != null);
此時newTable的結構為:
Entry[0] = A(.next) –> null
e不為null,繼續執行:
do {
// 此時e為B,e.next為A(第一個線程修改的),此行相當于next = A;
Entry<K,V> next = e.next;
// 假設i = 0
int i = indexFor(e.hash, newCapacity);
// newTable[0]為A,此行相當于B.next = A;
e.next = newTable[i];
// 此行相當于newTable[0] = B;
newTable[i] = e;
// 此行相當于e = A;
e = next;
} while (e != null);
此時newTable的結構為:
Entry[0] = B(.next) –> A(.next) –> null
e不為null,繼續執行:
do {
// 此時e為A,A.next為null,此行相當于next = null
Entry<K,V> next = e.next;
// 假設i = 0
int i = indexFor(e.hash, newCapacity);
// newTable[0]為B,此行相當于A.next = B;
e.next = newTable[i];
// 此行相當于newTable[0] = A;
newTable[i] = e;
// 此行相當于e = null;
e = next;
} while (e != null);
此時newTable的結構為:
Entry[0] = A(.next) <--> B(.next)
無限循環就此造成,此時如果有線程要執行HashMap.get(A) ,就會直接進入無限循環,耗光CPU。
從上面的分析可以看出,要導緻HashMap.get出現無限循環,還真有些不容易,至少要有兩個線程在同時進行put,同時觸發resize,并且之前一個桶裡的兩個相鄰的對象在容量變了後仍然hash到了同樣的數組位置中,這樣才有可能導緻問題,是以這現象隻偶爾出現(不過我們以前曾經悲催的出現過一個叢集重新開機,全部碰到了Velocity的那個錯誤使用HashMap的bug,導緻嚴重故障)
并發那點事
1、list線程安全類CopyOnWriteArrayList
java.util.concurrent.CopyOnWriteArrayList<E>
Add/Set
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();//擷取目前數組
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);//數組拷貝
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
周遊
public Iterator<E> iterator() {
//傳回一個new對象,類内部參數數組為目前數組,即如果目前list數組出現新增、删除
而周遊時不會出現同步異常,但會有髒資料出現 ex: CopyOnWriteArrayListTest
return new COWIterator<E>(getArray(), 0);
}