您還有心跳嗎?超時機制分析

2021-06-20 02:59:38 字數 3033 閱讀 5943

在c/s模式中,有時我們會長時間保持乙個連線,以避免頻繁地建立連線,但同時,一般會有乙個超時時間,在這個時間內沒發起任何請求的連線會被斷開,以減少負載,節約資源。並且該機制一般都是在服務端實現,因為client強制關閉或意外斷開連線,server端在此刻是感知不到的,如果放到client端實現,在上述情況下,該超時機制就失效了。本來這問題很普通,不太值得一提,但最近在專案中看到了該機制的一種糟糕的實現,故在此深入分析一下。

服務端一般會保持很多個連線,所以,一般是建立乙個定時器,定時檢查所有連線中哪些連線超時了。此外我們要做的是,當收到客戶端發來的資料時,怎麼去重新整理該連線的超時資訊?

最近看到一種實現方式是這樣做的:

public

class

connection

public

long getlasttime()

//......

}

在每次收到客戶端發來的資料時,呼叫refresh方法。

然後在定時器裡,用當前時間跟每個連線的getlasttime()作比較,來判定超時:

public

class

timeouttask

extends

timertask

}}

看到這,可能不少讀者已經看出問題來了,那就是記憶體可見性問題,呼叫refresh方法的執行緒跟執行定時器的執行緒肯定不是乙個執行緒,那run方法中讀到的lasttime就可能是舊值,即可能將活躍的連線判定超時,然後被乾掉。

有讀者此時可能想到了這樣乙個方法,將lasttime加個volatile修飾,是的,這樣確實解決了問題,不過,作為服務端,很多時候對效能是有要求的,下面來看下在我電腦上測出的一組資料,測試**如下,供參考

public

class performancetest

system.out.println(-time + (time = system.nanotime()));

for (int n = 0; n < test_size; n++)

vt++;

system.out.println(-time + (time = system.nanotime()));

for (int n = 0; n < test_size; n++)

vt = i;

system.out.println(-time + (time = system.nanotime()));

for (int n = 0; n < test_size; n++)

i = vt;

system.out.println(-time + (time = system.nanotime()));

for (int n = 0; n < test_size; n++)

i++;

system.out.println(-time + (time = system.nanotime()));

for (int n = 0; n < test_size; n++) i = n; system.out.println(-time + (time = system.nanotime())); } }

測試一千萬次,結果是(耗時單位:納秒,包含迴圈本身的時間): 238932949       volatile寫+取系統時間 144317590       普通寫+取系統時間 135596135       空的同步塊(synchronized) 80042382        volatile變數自增 15875140        volatile寫 6548994         volatile讀 2722555         普通自增 2949571         普通讀寫 從上面的資料看來,volatile寫+取系統時間的耗時是很高的,取系統時間的耗時也比較高,跟一次無競爭的同步差不多了,接下來分析下如何優化該超時時機。 首先:同步問題是肯定得考慮的,因為有跨執行緒的資料操作;另外,取系統時間的操作比較耗時,能否不在每次重新整理時都取時間?因為重新整理呼叫在高負載的情況下很頻繁。如果不在重新整理時取時間,那又該怎麼去判定超時? 我想到的辦法是,在refresh方法裡,僅設定乙個volatile的boolean變數reset(這應該是成本最小的了吧,因為要處理同步問題,要麼同步塊,要麼volatile,而volatile讀在此處是沒什麼意義的),對時間的掌控交給定時器來做,並為每個連線維護乙個計數器,每次加一,如果reset被設定為true了,則計數器歸零,並將reset設為false(因為計數器只由定時器維護,所以不需要做同步處理,從上面的測試資料來看,普通變數的操作,時間成本是很低的),如果計數器超過某個值,則判定超時。 下面給出具體的**:

public

class

connection } public

class

timeouttask

extends

timertask else

if (++c.count >= timeout_count)

;// timeout, do something}}

}

**中的timeout_count 等於超時時間除以定時器的週期,週期大小既影響定時器的執行頻率,也會影響實際超時時間的波動範圍(這個波動,第乙個方案也存在,也不太可能避免,並且也不需要多麼精確)。

**很簡潔,下面來分析一下。

reset加上了volatile,所以保證了多執行緒操作的可見性,雖然有兩個執行緒都對變數有寫操作,但無論這兩個執行緒怎麼穿插執行,都不會影響其邏輯含義。

再說下refresh方法,為什麼我在賦值語句上多加了個條件?這不是多了一次volatile讀操作嗎?我是這麼考慮的,高負載下,refresh會被頻繁呼叫,意味著reset長時間為true,那麼加上條件後,就不會執行寫操作了,只有一次讀操作,從上面的測試資料來看,volatile變數的讀操作的效能是顯著優於寫操作的。只不過在reset為false的時候,多了一次讀操作,但此情況在定時器的乙個週期內最多隻會發一次,而且對高負載情況下的優化顯然更有意義,所以我認為加上條件還是值得的。

最後提及一下,我有點完美主義,自認為上面的方案在我當前掌握的知識下,已經很漂亮了,如果你發現還有可優化的地方,或更好的方案,希望能分享。