由性能優化引起的活鎖現象

一、活鎖定義(源自百度百科)

活鎖指的是任務或者執行者沒有被阻塞,由於某些條件沒有滿足,導致一直重複嘗試,失敗,嘗試,失敗。 活鎖和死鎖的區別在於,處於活鎖的實體是在不斷的改變狀態,所謂的“活”, 而處於死鎖的實體表現爲等待;活鎖有可能自行解開,死鎖則不能。

活鎖可以認爲是一種特殊的飢餓。 下面這個例子在有的文章裏面認爲是活鎖。實際上這只是一種飢餓。因爲沒有體現出“活”的特點。 假設事務T2再不斷的重複嘗試獲取鎖R,那麼這個就是活鎖。
如果事務T1封鎖了數據R,事務T2又請求封鎖R,於是T2等待。T3也請求封鎖R,當T1釋放了R上的封鎖後,系統首先批准了T3的請求,T2仍然等待。然後T4又請求封鎖R,當T3釋放了R上的封鎖之後,系統又批准了T4的請求......T2可能永遠等待。

活鎖應該是一系列進程在輪詢地等待某個不可能爲真的條件爲真。活鎖的時候進程是不會blocked,這會導致耗盡CPU資源。

二、活鎖的例子

1、單一實體的活鎖

例如線程從隊列中拿出一個任務來執行,如果任務執行失敗,那麼將任務重新加入隊列,繼續執行。假設任務總是執行失敗,或者某種依賴的條件總是不滿足,那麼線程一直在繁忙卻沒有任何結果。

2、協同導致的活鎖

生活中的典型例子: 兩個人在窄路相遇,同時向一個方向避讓,然後又向另一個方向避讓,如此反覆。
通信中也有類似的例子,多個用戶共享信道(最簡單的例子是大家都用對講機),同一時刻只能有一方發送信息。發送信號的用戶會進行衝突檢測, 如果發生衝突,就選擇避讓,然後再發送。 假設避讓算法不合理,就導致每次發送,都衝突,避讓後再發送,還是衝突。
計算機中的例子:兩個線程發生了某些條件的碰撞後重新執行,那麼如果再次嘗試後依然發生了碰撞,長此下去就有可能發生活鎖。

三、示例說明((下述的例子不一定就要去優化,只是直覺上的一種衝動))

類 Test 變量聲明:

private static short counter = (short) 0;
方法定義:
protected static short getCount() {
synchronized(Test.class) {
if (counter<0) counter=0;
return counter++;
}
}
看到上述寫法,就有優化的衝動:

private static AtomicInteger counter = new AtomicInteger();

protected static short getCount() {
int i = counter.getAndIncrement();
while (i >= Short.MAX_VALUE) {
counter.compareAndSet(i, 0);//------------------------------CAS指令1
i = counter.getAndIncrement();//-----------------------------CAS指令2
}
return (short)i;
}
併發測試,在某種情況下出現了錯誤,直到 i 成爲了一個負數,也完全違背了這個方法設計的初衷。---------------返回一個非負Short值。
指令1 和指令2 在不同的線程間切換調用。出現活鎖現象(雖然最終退出程序,卻得到一個錯誤的結果。)

四、活鎖的解決方法

1、解決協同活鎖的一種方案是調整重試機制。

比如引入一些隨機性。例如如果檢測到衝突,那麼就暫停隨機的一定時間進行重試。這回大大減少碰撞的可能性。
 典型的例子是以太網的CSMA/CD檢測機制。

2、另外爲了避免可能的死鎖,適當加入一定的重試次數也是有效的解決辦法。

儘管這在業務上會引起一些複雜的邏輯處理。
比如約定重試機制避免再次衝突。 例如自動駕駛的防碰撞系統(假想的例子),可以根據序列號約定檢測到相撞風險時,序列號小的飛機朝上飛, 序列號大的飛機朝下飛。

五、一些啓示

性能優化有風險,測試需謹慎。多線程編程中,最容易注重的地方就是避免產生死鎖現象,而對於線程飢餓或是活鎖現象一般較少顧及。需要引起足夠的重視才行。
在確實有證據證明存在性能瓶頸的地方,優化以後完整,完善的測試必不可少。

發佈了138 篇原創文章 · 獲贊 2 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章