提升Java的鎖性能

  幾個月前我們介紹瞭如何通過Plumbr來進行線程鎖檢測,隨後便收到了很多類似的問題,“Hi,文章寫得不錯,現在我終於知道是什麼引發的性能問題了,但是現在我該怎麼做?”

  爲了在我們的產品中集成這個解決方案,我們付出了許多努力,不過在本文中,我想給大家分享幾個常用的優化技巧,而不一定非要使用我們這款鎖檢測的工具。包括分拆鎖,併發數據結構,保護數據而非代碼,以及縮小鎖的作用域。

  鎖無罪,競爭其罪

  如果你在多線程代碼中碰到了性能問題,你肯定會先抱怨鎖。畢竟,從“常識”來講,鎖的性能是很差的,並且還限制了程序的可伸縮性。如果你懷揣着這樣的想法去優化代碼並刪除鎖的話,最後你肯定會引入一些難纏的併發BUG

  因此分清楚競爭鎖與無競爭鎖的區別是很有必要的。如果一個線程嘗試進入另一個線程正在執行的同步塊或者方法時,便會出現鎖競爭。第二個線程就必須等待前一個線程執行完這個同步塊並釋放掉監視器(monitor。如果只有一個線程在執行這段同步的代碼,這個鎖就是無競爭的。

  事實上,JVM中的同步已經針對這種無競爭的情況進行了優化,對於絕大多數應用而言,無競爭的鎖幾乎是沒有任何額外的開銷的。因此,出了性能問題不能光怪鎖,你得怪競爭鎖。在明確了這點以後 ,我們來看下如何能減少鎖的競爭或者競爭的時間。

  保護數據而非代碼

  實現線程安全最快的方法就是直接將整個方法上鎖。比如說下面的這個例子,這是在線撲克遊戲服務端的一個簡單的實現:

class GameServer {

public Map<<String, List<Player>> tables = new HashMap<String, List<Player>>();

public synchronized void join(Player player, Table table) {

if (player.getAccountBalance() > table.getLimit()) {

List<Player> tablePlayers = tables.get(table.getId());

if (tablePlayers.size() < 9) {

tablePlayers.add(player);

}

}

}

public synchronized void leave(Player player, Table table) {/*body skipped for brevity*/}

public synchronized void createTable() {/*body skipped for brevity*/}

public synchronized void destroyTable(Table table) {/*body skipped for brevity*/}

}

  作者的想法是好的——就是當新的玩家加入的時候,必須得保證桌上的玩家的數量不能超過9個。

  不過這個上鎖的方案更適合加到牌桌上,而不是玩家進入的時候——即便是在一個流量一般的撲克網站上,這樣的系統也肯定會由於線程等待鎖釋放而頻繁地觸發競爭事件。被鎖住的代碼塊包含了帳戶餘額以及牌桌上限的檢查,這裏面很可能會包括一些很昂貴的操作,這樣不僅會容易觸發競爭並且使得競爭的時間變長。

  解決問題的第一步就是要確保你保護的是數據,而不是代碼,先將同步從方法聲明移到方法體裏。在上面這個簡短的例子中,剛開始好像能修改的地方並不多。不過我們考慮的是整個GameServer類,而不只限於這個join()方法: 

class GameServer {

public Map<String, List<Player>> tables = new HashMap<String, List<Player>>();

public void join(Player player, Table table) {

synchronized (tables) {

if (player.getAccountBalance() > table.getLimit()) {

List<Player> tablePlayers = tables.get(table.getId());

if (tablePlayers.size() < 9) {

tablePlayers.add(player);

}

}

}

}

public void leave(Player player, Table table) {/* body skipped for brevity */}

public void createTable() {/* body skipped for brevity */}

public void destroyTable(Table table) {/* body skipped for brevity */}

}

  這看似一個很小的改動,卻會影響到整個類的行爲。當玩家加入牌桌 時,前面那個同步的方法會鎖在GameServerthis實例上,並與同時想離開牌桌(leave)的玩家產生競爭行爲。而將鎖從方法簽名移到方法內部以後,則將上鎖的時機往後推遲了,一定程度上減小了競爭的可能性。

  縮小鎖的作用域

  現在我們已經確保保護的是數據而不是代碼了,我們得再確認鎖住的部分都是必要的——比如說,代碼可以重寫成這樣 :

public class GameServer {

public Map<String, List<Player>> tables = new HashMap<String, List<Player>>();

public void join(Player player, Table table) {

if (player.getAccountBalance() > table.getLimit()) {

synchronized (tables) {

List<Player> tablePlayers = tables.get(table.getId());

if (tablePlayers.size() < 9) {

tablePlayers.add(player);

}

}

}

}

//other methods skipped for brevity

}

  現在檢查玩家餘額的這個耗時操作就在鎖作用域外邊了。注意到了吧,鎖的引入其實只是爲了保護玩家數量不超過桌子的容量而已,檢查帳戶餘額這個事情並不在要保護的範圍之內。

  分拆鎖

  再看下上面這段代碼,你會注意到整個數據結構都被同一個鎖保護起來了。考慮到這個數據結構中可能會存有上千張牌桌,出現競爭的概率還是非常高的,因此保護每張牌桌不超出容量的工作最好能分別來進行。

  對於這個例子而言,爲每張桌子分配一個獨立的鎖並非難事,代碼如下: 

public class GameServer {

public Map<String, List<Player>> tables = new HashMap<String, List<Player>>();

public void join(Player player, Table table) {

if (player.getAccountBalance() > table.getLimit()) {

List<Player> tablePlayers = tables.get(table.getId());

synchronized (tablePlayers) {

if (tablePlayers.size() < 9) {

tablePlayers.add(player);

}

}

}

}

//other methods skipped for brevity

}

  現在我們把對所有桌子同步的操作變成了只對同一張桌子進行同步,因此出現鎖競爭的概率就大大減小了。如果說桌子中有100張桌子的話,那麼現在出現競爭的概率就小了100倍。

  使用併發的數據結構

  另一個可以改進的地方就是棄用傳統的單線程的數據結構,改爲使用專門爲併發所設計的數據結構。比如說,可以用ConcurrentHashMap來存儲所有的撲克桌,這樣代碼就會變成這樣:

 

public class GameServer {

public Map<String, List<Player>> tables = new ConcurrentHashMap<String, List<Player>>();

public synchronized void join(Player player, Table table) {/*Method body skipped for brevity*/}

public synchronized void leave(Player player, Table table) {/*Method body skipped for brevity*/}

public synchronized void createTable() {

Table table = new Table();

tables.put(table.getId(), table);

}

public synchronized void destroyTable(Table table) {

tables.remove(table.getId());

}

}

  join()leave()方法的同步操作變得更簡單了,因爲我們現在不用再對tables進行加鎖了,這都多虧了ConcurrentHashMap。然而,我們還是要保證每個tablePlayers的一致性。因此這個地方ConcurrentHashMap幫不上什麼忙。同時我們還得在createTable()destroyTable()方法中創建新的桌子以及銷燬桌子,這對ConcurrentHashMap而言本身就是併發的,因此你可以並行地增加或者減少桌子的數量。

  其它的技巧及方法

  降低鎖的可見性。在上述例子中,鎖是聲明爲public的,因此可以被別人所訪問到,你所精心設計的監視器可能會被別人鎖住,從而功虧一簣。

  看一下java.util.concurrent.locks包下面有哪些鎖策略對你是有幫助的。

使用原子操作。上面這個例子中的簡單的計數器其實並不需要進行加鎖。將計數的Integer換成AtomicInteger對這個場景來說就綽綽有餘了。

本文轉自:http://www.spasvo.com/news/html/2015126132124.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章