天天看點

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

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

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

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

<code>01</code>

<code>public</code> <code>class</code> <code>connection {</code>

<code>02</code>

<code>    </code><code>private</code> <code>long</code> <code>lasttime;</code>

<code>03</code>

<code>    </code><code>public</code> <code>void</code> <code>refresh() {</code>

<code>04</code>

<code>        </code><code>lasttime = system.currenttimemillis();</code>

<code>05</code>

<code>    </code><code>}</code>

<code>06</code>

<code>07</code>

<code>    </code><code>public</code> <code>long</code> <code>getlasttime() {</code>

<code>08</code>

<code>        </code><code>return</code> <code>lasttime;</code>

<code>09</code>

<code>10</code>

<code>    </code><code>//......</code>

<code>11</code>

<code>}</code>

在每次收到用戶端發來的資料時,調用refresh方法。

然後在定時器裡,用目前時間跟每個連接配接的getlasttime()作比較,來判定逾時:

<code>1</code>

<code>public</code> <code>class</code> <code>timeouttask  </code><code>extends</code> <code>timertask{</code>

<code>2</code>

<code>    </code><code>public</code> <code>void</code> <code>run() {</code>

<code>3</code>

<code>        </code><code>long</code> <code>now = system.currenttimemillis();</code>

<code>4</code>

<code>        </code><code>for</code><code>(connection c: connections){</code>

<code>5</code>

<code>            </code><code>if</code><code>(now - c.getlasttime()&gt; timeout_threshold)</code>

<code>6</code>

<code>                </code><code>;</code><code>//timeout, do something</code>

<code>7</code>

<code>        </code><code>}</code>

<code>8</code>

<code>9</code>

看到這,可能不少讀者已經看出問題來了,那就是記憶體可見性問題,調用refresh方法的線程跟執行定時器的線程肯定不是一個線程,那run方法中讀到的lasttime就可能是舊值,即可能将活躍的連接配接判定逾時,然後被幹掉。

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

<code>public</code> <code>class</code> <code>performancetest {</code>

<code>    </code><code>private</code> <code>static</code> <code>long</code> <code>i;</code>

<code>    </code><code>private</code> <code>volatile</code> <code>static</code> <code>long</code> <code>vt;</code>

<code>    </code><code>private</code> <code>static</code> <code>final</code> <code>int</code> <code>test_size = </code><code>10000000</code><code>;</code>

<code>    </code><code>public</code> <code>static</code> <code>void</code> <code>main(string[] args) {</code>

<code>        </code><code>long</code> <code>time = system.nanotime();</code>

<code>        </code><code>for</code> <code>(</code><code>int</code> <code>n = </code><code>0</code><code>; n &lt; test_size; n++)</code>

<code>            </code><code>vt = system.currenttimemillis();</code>

<code>        </code><code>system.out.println(-time + (time = system.nanotime()));</code>

<code>12</code>

<code>            </code><code>i = system.currenttimemillis();</code>

<code>13</code>

<code>14</code>

<code>15</code>

<code>            </code><code>synchronized</code> <code>(performancetest.</code><code>class</code><code>) {</code>

<code>16</code>

<code>            </code><code>}</code>

<code>17</code>

<code>18</code>

<code>19</code>

<code>            </code><code>vt++;</code>

<code>20</code>

<code>21</code>

<code>22</code>

<code>            </code><code>vt = i;</code>

<code>23</code>

<code>24</code>

<code>25</code>

<code>            </code><code>i = vt;</code>

<code>26</code>

<code>27</code>

<code>28</code>

<code>            </code><code>i++;</code>

<code>29</code>

<code>30</code>

<code>31</code>

<code>            </code><code>i = n;</code>

<code>32</code>

<code>33</code>

<code>34</code>

測試一千萬次,結果是(耗時機關:納秒,包含循環本身的時間):

238932949       volatile寫+取系統時間

144317590       普通寫+取系統時間

135596135       空的同步塊(synchronized)

80042382        volatile變量自增

15875140        volatile寫

6548994         volatile讀

2722555         普通自增

2949571         普通讀寫

從上面的資料看來,volatile寫+取系統時間的耗時是很高的,取系統時間的耗時也比較高,跟一次無競争的同步差不多了,接下來分析下如何優化該逾時時機。

首先:同步問題是肯定得考慮的,因為有跨線程的資料操作;另外,取系統時間的操作比較耗時,能否不在每次重新整理時都取時間?因為重新整理調用在高負載的情況下很頻繁。如果不在重新整理時取時間,那又該怎麼去判定逾時?

我想到的辦法是,在refresh方法裡,僅設定一個volatile的boolean變量reset(這應該是成本最小的了吧,因為要處理同步問題,要麼同步塊,要麼volatile,而volatile讀在此處是沒什麼意義的),對時間的掌控交給定時器來做,并為每個連接配接維護一個計數器,每次加一,如果reset被設定為true了,則計數器歸零,并将reset設為false(因為計數器隻由定時器維護,是以不需要做同步處理,從上面的測試資料來看,普通變量的操作,時間成本是很低的),如果計數器超過某個值,則判定逾時。 下面給出具體的代碼:

<code>    </code><code>int</code> <code>count = </code><code>0</code><code>;</code>

<code>    </code><code>volatile</code> <code>boolean</code> <code>reset = </code><code>false</code><code>;</code>

<code>        </code><code>if</code> <code>(reset == </code><code>false</code><code>)</code>

<code>            </code><code>reset = </code><code>true</code><code>;</code>

<code>public</code> <code>class</code> <code>timeouttask </code><code>extends</code> <code>timertask {</code>

<code>        </code><code>for</code> <code>(connection c : connections) {</code>

<code>            </code><code>if</code> <code>(c.reset) {</code>

<code>                </code><code>c.reset = </code><code>false</code><code>;</code>

<code>                </code><code>c.count = </code><code>0</code><code>;</code>

<code>            </code><code>} </code><code>else</code> <code>if</code> <code>(++c.count &gt;= timeout_count)</code>

<code>                </code><code>;</code><code>// timeout, do something</code>

代碼中的timeout_count 等于逾時時間除以定時器的周期,周期大小既影響定時器的執行頻率,也會影響實際逾時時間的波動範圍(這個波動,第一個方案也存在,也不太可能避免,并且也不需要多麼精确)。

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

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

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

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

————————————-

補充一下:一般情況下,也可用特定的心跳包來重新整理,而不是每次收到消息都重新整理,這樣一來,重新整理頻率就很低了,也就沒必要太在乎性能開銷。