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

問題描述編程

C/S模式中,有時咱們會長時間保持一個鏈接,以免頻繁地創建鏈接,但同時,通常會有一個超時時間,在這個時間內沒發起任何請求的鏈接會被斷開,以減小負載,節約資源。而且該機制通常都是在服務端實現,由於client強制關閉或意外斷開鏈接,server端在此刻是感知不到的,若是放到client端實現,在上述狀況下,該超時機制就失效了。原本這問題很普通,不太值得一提,但最近在項目中看到了該機制的一種糟糕的實現,故在此深刻分析一下。多線程

問題分析及解決方案併發

服務端通常會保持不少個鏈接,因此,通常是建立一個定時器,定時檢查全部鏈接中哪些鏈接超時了。此外咱們要作的是,當收到客戶端發來的數據時,怎麼去刷新該鏈接的超時信息?ide

最近看到一種實現方式是這樣作的:性能

  1. public class Connection {
    測試

  2.         private long lastTime;
    優化

  3.         public void refresh() {
    spa

  4.                 lastTime = System.currentTimeMillis();
    線程

  5.         }
    orm


  6.         public long getLastTime() {

  7.                 return lastTime;

  8.         }

  9.         //......

  10. }

複製代碼

在每次收到客戶端發來的數據時,調用refresh方法。

而後在定時器裏,用當前時間跟每一個鏈接的getLastTime()做比較,來斷定超時:

  1. public class TimeoutTask  extends TimerTask{

  2.         public void run() {

  3.                 long now = System.currentTimeMillis();

  4.                 for(Connection c: connections){

  5.                         if(now - c.getLastTime()> TIMEOUT_THRESHOLD)

  6.                                 ;//timeout, do something

  7.                 }

  8.         }

  9. }

複製代碼

看到這,可能很多讀者已經看出問題來了,那就是內存可見性問題,調用refresh方法的線程跟執行定時器的線程確定不是一個線程,那run方法中讀到的lastTime就多是舊值,便可能將活躍的鏈接斷定超時,而後被幹掉。

有讀者此時可能想到了這樣一個方法,將lastTime加個volatile修飾,是的,這樣確實解決了問題,不過,做爲服務端,不少時候對性能是有要求的,下面來看下在我電腦上測出的一組數據,測試代碼以下,供參考

  1. public class PerformanceTest {

  2.         private static long i;

  3.         private volatile static long vt;

  4.         private static final int TEST_SIZE = 10000000;


  5.         public static void main(String[] args) {

  6.                 long time = System.nanoTime();

  7.                 for (int n = 0; n < TEST_SIZE; n++)

  8.                         vt = System.currentTimeMillis();

  9.                 System.out.println(-time + (time = System.nanoTime()));

  10.                 for (int n = 0; n < TEST_SIZE; n++)

  11.                         i = System.currentTimeMillis();

  12.                 System.out.println(-time + (time = System.nanoTime()));

  13.                 for (int n = 0; n < TEST_SIZE; n++)

  14.                         synchronized (PerformanceTest.class) {

  15.                         }

  16.                 System.out.println(-time + (time = System.nanoTime()));

  17.                 for (int n = 0; n < TEST_SIZE; n++)

  18.                         vt++;

  19.                 System.out.println(-time + (time = System.nanoTime()));

  20.                 for (int n = 0; n < TEST_SIZE; n++)

  21.                         vt = i;

  22.                 System.out.println(-time + (time = System.nanoTime()));

  23.                 for (int n = 0; n < TEST_SIZE; n++)

  24.                         i = vt;

  25.                 System.out.println(-time + (time = System.nanoTime()));

  26.                 for (int n = 0; n < TEST_SIZE; n++)

  27.                         i++;

  28.                 System.out.println(-time + (time = System.nanoTime()));

  29.                 for (int n = 0; n < TEST_SIZE; n++)

  30.                         i = n;

  31.                 System.out.println(-time + (time = System.nanoTime()));

  32.         }

  33. }

複製代碼

測試一千萬次,結果是(耗時單位:納秒,包含循環自己的時間):
238932949       volatile寫+取系統時間
144317590       普通寫+取系統時間
135596135       空的同步塊(synchronized)
80042382        volatile變量自增
15875140        volatile寫
6548994         volatile讀
2722555         普通自增
2949571         普通讀寫

從上面的數據看來,volatile寫+取系統時間的耗時是很高的,取系統時間的耗時也比較高,跟一次無競爭的同步差很少了,接下來分析下如何優化該超時時機。

首先:同步問題是確定得考慮的,由於有跨線程的數據操做;另外,取系統時間的操做比較耗時,可否不在每次刷新時都取時間?由於刷新調用在高負載的狀況下很頻繁。若是不在刷新時取時間,那又該怎麼去斷定超時?

我想到的辦法是,在refresh方法裏,僅設置一個volatile的boolean變量reset(這應該是成本最小的了吧,由於要處理同步問題,要麼同步塊,要麼volatile,而volatile讀在此處是沒什麼意義的),對時間的掌控交給定時器來作,併爲每一個鏈接維護一個計數器,每次加一,若是reset被設置爲true了,則計數器歸零,並將reset設爲false(由於計數器只由定時器維護,因此不須要作同步處理,從上面的測試數據來看,普通變量的操做,時間成本是很低的),若是計數器超過某個值,則斷定超時。 下面給出具體的代碼:

  1. public class Connection {

  2.         int count = 0;

  3.         volatile boolean reset = false;

  4.         public void refresh() {

  5.                 if (reset == false)

  6.                         reset = true;

  7.         }

  8. }


  9. public class TimeoutTask extends TimerTask {

  10.         public void run() {

  11.                 for (Connection c : connections) {

  12.                         if (c.reset) {

  13.                                 c.reset = false;

  14.                                 c.count = 0;

  15.                         } else if (++c.count >= TIMEOUT_COUNT)

  16.                                 ;// timeout, do something

  17.                 }

  18.         }

  19. }

複製代碼

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

代碼很簡潔,下面來分析一下。

reset加上了volatile,因此保證了多線程操做的可見性,雖然有兩個線程都對變量有寫操做,但不管這兩個線程怎麼穿插執行,都不會影響其邏輯含義。

再說下refresh方法,爲何我在賦值語句上多加了個條件?這不是多了一次volatile讀操做嗎?我是這麼考慮的,高負載下,refresh會被頻繁調用,意味着reset長時間爲true,那麼加上條件後,就不會執行寫操做了,只有一次讀操做,從上面的測試數據來看,volatile變量的讀操做的性能是顯著優於寫操做的。只不過在reset爲false的時候,多了一次讀操做,但此狀況在定時器的一個週期內最多隻會發一次,並且對高負載狀況下的優化顯然更有意義,因此我認爲加上條件仍是值得的。

最後說起一下,我有點完美主義,自認爲上面的方案在我當前掌握的知識下,已經很漂亮了,若是你發現還有可優化的地方,或更好的方案,但願能分享。
————————————-
補充一下:通常狀況下,也可用特定的心跳包來刷新,而不是每次收到消息都刷新,這樣一來,刷新頻率就很低了,也就不必太在意性能開銷。

原創文章,轉載請註明: 轉載自併發編程網 – ifeve.com

相關文章
相關標籤/搜索