摘要:問題分析及解決方案服務端一般會保持很多個連接,所以,一般是創建一個定時器,定時檢查所有連接中哪些連接超時了。
問題描述
在C/S模式中,有時我們會長時間保持一個連接,以避免頻繁地建立連接,但同時,一般會有一個超時時間,在這個時間內沒發起任何請求的連接會被斷開,以減少負載,節約資源。并且該機制一般都是在服務端實現,因為client強制關閉或意外斷開連接,server端在此刻是感知不到的,如果放到client端實現,在上述情況下,該超時機制就失效了。本來這問題很普通,不太值得一提,但最近在項目中看到了該機制的一種糟糕的實現,故在此深入分析一下。
問題分析及解決方案服務端一般會保持很多個連接,所以,一般是創建一個定時器,定時檢查所有連接中哪些連接超時了。此外我們要做的是,當收到客戶端發來的數據時,怎么去刷新該連接的超時信息?
最近看到一種實現方式是這樣做的:
public class Connection { private long lastTime; public void refresh() { lastTime = System.currentTimeMillis(); } public long getLastTime() { return lastTime; } //...... }
在每次收到客戶端發來的數據時,調用refresh方法。
然后在定時器里,用當前時間跟每個連接的getLastTime()作比較,來判定超時:
public class TimeoutTask extends TimerTask{ public void run() { long now = System.currentTimeMillis(); for(Connection c: connections){ if(now - c.getLastTime()> TIMEOUT_THRESHOLD) ;//timeout, do something } } }
看到這,可能不少讀者已經看出問題來了,那就是內存可見性問題,調用refresh方法的線程跟執行定時器的線程肯定不是一個線程,那run方法中讀到的lastTime就可能是舊值,即可能將活躍的連接判定超時,然后被干掉。
有讀者此時可能想到了這樣一個方法,將lastTime加個volatile修飾,是的,這樣確實解決了問題,不過,作為服務端,很多時候對性能是有要求的,下面來看下在我電腦上測出的一組數據,測試代碼如下,供參考
public class PerformanceTest { private static long i; private volatile static long vt; private static final int TEST_SIZE = 10000000; public static void main(String[] args) { long time = System.nanoTime(); for (int n = 0; n < TEST_SIZE; n++) vt = System.currentTimeMillis(); System.out.println(-time + (time = System.nanoTime())); for (int n = 0; n < TEST_SIZE; n++) i = System.currentTimeMillis(); System.out.println(-time + (time = System.nanoTime())); for (int n = 0; n < TEST_SIZE; n++) synchronized (PerformanceTest.class) { } 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 { int count = 0; volatile boolean reset = false; public void refresh() { if (reset == false) reset = true; } } public class TimeoutTask extends TimerTask { public void run() { for (Connection c : connections) { if (c.reset) { c.reset = false; c.count = 0; } else if (++c.count >= TIMEOUT_COUNT) ;// timeout, do something } } }
代碼中的TIMEOUT_COUNT 等于超時時間除以定時器的周期,周期大小既影響定時器的執行頻率,也會影響實際超時時間的波動范圍(這個波動,第一個方案也存在,也不太可能避免,并且也不需要多么精確)。
代碼很簡潔,下面來分析一下。
reset加上了volatile,所以保證了多線程操作的可見性,雖然有兩個線程都對變量有寫操作,但無論這兩個線程怎么穿插執行,都不會影響其邏輯含義。
再說下refresh方法,為什么我在賦值語句上多加了個條件?這不是多了一次volatile讀操作嗎?我是這么考慮的,高負載下,refresh會被頻繁調用,意味著reset長時間為true,那么加上條件后,就不會執行寫操作了,只有一次讀操作,從上面的測試數據來看,volatile變量的讀操作的性能是顯著優于寫操作的。只不過在reset為false的時候,多了一次讀操作,但此情況在定時器的一個周期內最多只會發一次,而且對高負載情況下的優化顯然更有意義,所以我認為加上條件還是值得的。
最后提及一下,我有點完美主義,自認為上面的方案在我當前掌握的知識下,已經很漂亮了,如果你發現還有可優化的地方,或更好的方案,希望能分享。
via ifeve
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/69761.html
摘要:基礎何為心跳顧名思義所謂心跳即在長連接中客戶端和服務器之間定期發送的一種特殊的數據包通知對方自己還在線以確保連接的有效性為什么需要心跳因為網絡的不可靠性有可能在保持長連接的過程中由于某些突發情況例如網線被拔出突然掉電等會造成服務器和客戶端的 基礎 何為心跳 顧名思義, 所謂 心跳, 即在 TCP 長連接中, 客戶端和服務器之間定期發送的一種特殊的數據包, 通知對方自己還在線, 以確保 ...
摘要:和斷開,處理措施不一樣,會分別做出重連和關閉通道的操作。取消定時器取消大量已排隊任務,用于回收空間該方法是停止現有心跳,也就是停止定時器,釋放空間。做到異步處理返回結果時能給準確的返回給對應的請求。 遠程通訊——Exchange層 目標:介紹Exchange層的相關設計和邏輯、介紹dubbo-remoting-api中的exchange包內的源碼解析。 前言 上一篇文章我講的是dubb...
摘要:比如面向連接的功能包發送接收數量包發送接收速率錯誤計數連接重連次數調用延遲連接狀態等。你要處理的,就是心跳超時的邏輯,比如延遲重連。發生異常后,可以根據不同的類型選擇斷線重連比如一些二進制協議的編解碼紊亂問題,或者調度到其他節點。 在java界,netty無疑是開發網絡應用的拿手菜。你不需要太多關注復雜的nio模型和底層網絡的細節,使用其豐富的接口,可以很容易的實現復雜的通訊功能。 和...
閱讀 1156·2023-04-25 17:28
閱讀 3531·2021-10-14 09:43
閱讀 3955·2021-10-09 10:02
閱讀 1943·2019-08-30 14:04
閱讀 3129·2019-08-30 13:09
閱讀 3270·2019-08-30 12:53
閱讀 2896·2019-08-29 17:11
閱讀 1823·2019-08-29 16:58