2011-01-05 81 views
29

我正在爲多個玩家(線程)同時移動的遊戲建模。 當前玩家所在位置的信息將被存儲兩次:玩家有一個變量「hostField」,它指向棋盤上的一個字段,每個字段都有一個ArrayList存儲當前位於該字段的玩家。同步塊 - 鎖定多個對象

對於我有冗餘信息這一事實,我並不滿意,但是我發現沒有辦法避免這種情況,而無需循環訪問大數據集。但是,當一個玩家從一個領域移動到另一個領域時,我想確保(1)冗餘信息保持關聯(2)此刻沒有其他人正在操縱該領域。

因此,我需要做的是這樣

synchronized(player, field) { 
    // code 
} 

這是不可能的,對不對?

我該怎麼辦? :)

+3

由於操作非常短暫,您可能會發現只使用一個全局鎖定表現得足夠好,以至於無法區分這些差異。使用一個鎖可能會限制自己每秒移動200K(這是否足夠?),但可能會簡化您的代碼,並且不會發生死鎖。 – 2011-01-05 12:52:06

回答

19

我添加了另一個答案,因爲我還不能添加評論給其他人的帖子。

  • 實際上,同步是用於代碼而不是對象或數據。用作同步塊中的參數的對象引用表示鎖。

所以如果你有這樣的代碼:

class Player { 

    // Same instance shared for all players... Don't show how we get it now. 
    // Use one dimensional board to simplify, doesn't matter here. 
    private List<Player>[] fields = Board.getBoard(); 

    // Current position 
    private int x; 

    public synchronized int getX() { 
    return x; 
    } 

    public void setX(int x) { 
    synchronized(this) { // Same as synchronized method 
     fields[x].remove(this); 
     this.x = x; 
     field[y].add(this); 
    } 
    } 
} 

然後儘管同步塊beeing訪問現場沒有受到保護,因爲鎖是不一樣的(它是在不同的情況下)。因此,您的主板的玩家列表可能會變得不一致並導致運行時異常。

相反,如果你寫了下面的代碼,它會奏效,因爲我們只有一個爲所有玩家共享鎖:

class Player { 

    // Same instance shared for all players... Don't show how we get it now. 
    // Use one dimensional board to simplify, doesn't matter here. 
    private List<Player>[] fields; 

    // Current position 
    private int x; 

    private static Object sharedLock = new Object(); // Any object's instance can be used as a lock. 

    public int getX() { 
    synchronized(sharedLock) { 
     return x; 
    } 
    } 

    public void setX(int x) { 
    synchronized(sharedLock) { 
     // Because of using a single shared lock, 
     // several players can't access fields at the same time 
     // and so can't create inconsistencies on fields. 
     fields[x].remove(this); 
     this.x = x; 
     field[y].add(this); 
    } 
    } 
} 

=>請務必使用只有一個鎖來訪問所有播放機或董事會的狀態將不一致。

+0

是的,使用一個鎖可能會工作(以及鎖定整個字段或鎖定Player.class)。我害怕我不能這樣做,因爲我的練習的規範說我不能使用太大的鎖......我甚至嘗試了小鎖與大鎖,但發現我的程序運行得更快與小鎖。再次感謝您長久的回答!我很樂意支持你獲得積分! :) – speendo 2011-01-05 20:15:12

44

一個平凡的解決辦法是

synchronized(player) { 
    synchronized(field) { 
     // code 
    } 
} 

但是,請確保您始終相同的順序,以避免死鎖鎖定資源。

請注意,在實踐中,瓶頸是領域,所以對領域(或專用的普通鎖物體,如@ ripper234正確指出)的單一鎖定可能就足夠了(除非您同時操縱其他玩家,衝突的方式)。

+1

我只是想寫這個,並補充說你應該使用專用的鎖對象,而不是鎖定在業務對象本身,當兩名工作人員從工作中彈出並需要我的幫助:)有聲望提升。 – ripper234 2011-01-05 12:41:57

+0

+1:直指點。作爲一個方面說明:可以建立一個有兩個場的類,並將其用作玩家位置模型。有了這個,只能有一個鎖,但它可能根本不適合現有的編程模型。 – Rekin 2011-01-05 12:42:33

+2

@ ripper234,哦那些同事 - 總是困擾於瑣事和搶奪我們的寶貴時間,否則我們可以平靜地花在SO上;-) – 2011-01-05 12:47:23

1

你不應該爲你的建模感到不好 - 這只是一個雙向導航關聯。

如果你照顧(如在其他答案中告訴的)操縱原子,例如在Field方法中,這很好。

 

public class Field { 

    private Object lock = new Object(); 

    public removePlayer(Player p) { 
    synchronized (lock) { 
     players.remove(p); 
     p.setField(null); 
    } 
    } 

    public addPlayer(Player p) { 
    synchronized (lock) { 
     players.add(p); 
     p.setField(this); 
    } 
    } 
} 

 

如果「Player.setField」受到保護,那就沒問題了。

如果您需要「移動」語義的進一步原子性,請上一層爲板子。

+0

我並沒有完全理解「private Object lock = new Object();」模式。我之前讀過它,但並沒有真正理解它......但是如果我確定了你的話,那麼你只需確保兩個變化同時發生,對吧?你爲什麼不鎖定Field而是「鎖定」? – speendo 2011-01-05 13:47:47

+1

@Marcel as @ ripper234已經說過 - 這是一個很好的做法,因爲它隱藏了用於從公共訪問進行鎖定的顯示器。任何人都可以鎖定字段實例,只能鎖定其私有「鎖定」字段。在你的例子中,這沒有什麼區別。只是用它作爲優秀的風格。這種模式稍後將例如允許通過使用更多的鎖對象等來增加更復雜場景中的併發。 – mtraut 2011-01-05 13:58:17

7

使用併發性時,始終難以給出良好的響應。這很大程度上取決於你真正在做什麼以及真正重要的事情。

從我的理解,一個球員的舉動包括:

1 Updading球員的位置。

2從前一場中移除玩家。

3將玩家添加到新的領域。

想象一下,你在同一時間,但AQUIRE只有一個使用幾個鎖在一個時間: - 另一名球員能夠很好地看在錯誤的時刻,基本上1 & 2或2 & 3.有些玩家之間可以出現已經disapeared從董事會爲例。

想象一下你做imbricked鎖定這樣的:

synchronized(player) { 
    synchronized(previousField) { 
    synchronized(nextField) { 
     ... 
    } 
    } 
} 

問題是...它不工作,看執行這個命令爲2個線程:

Thread1 : 
Lock player1 
Lock previousField 
Thread2 : 
Lock nextField and see that player1 is not in nextField. 
Try to lock previousField and so way for Thread1 to release it. 
Thread1 : 
Lock nextField 
Remove player1 from previous field and add it to next field. 
Release all locks 
Thread 2 : 
Aquire Lock on previous field and read it : 

線程2認爲玩家1從整個董事會中消失。如果這對於您的應用程序是個問題,那麼您不能使用此解決方案。

imbriqued鎖定的其他問題:線程可能會卡住。 想象2個播放器:它們在完全相同的時間交換它們的位置:

player1 aquire it's own position at the same time 
player2 aquire it's own position at the same time 
player1 try to acquire player2 position : wait for lock on player2 position. 
player2 try to acquire player1 position : wait for lock on player1 position. 

=>兩位球員將被阻止。

在我看來,最好的解決方案是隻使用一個鎖,用於整個遊戲狀態。

當玩家想要讀取狀態時,它會鎖定整個遊戲狀態(玩家&板),並根據自己的用法制作副本。然後它可以沒有任何鎖定地處理。

當玩家想要寫入狀態時,它會鎖定整個遊戲狀態,寫入新狀態,然後釋放鎖定。

=>鎖定限於遊戲狀態的讀/寫操作。玩家可以在自己的副本上進行「長時間」的棋盤狀態檢查。

這可以防止任何不一致的狀態,例如幾個字段中的玩家或沒有玩家,但不阻止該玩家可以使用「舊」狀態。

它可能看起來很奇怪,但它是一個象棋遊戲的典型案例。當你等待其他玩家移動時,你會看到棋盤在移動之前。你不知道其他玩家會做什麼動作,直到他終於移動了,你纔會處於「舊」狀態。

+0

哇!你做了很大的努力來回答我的問題!非常感謝你!!! – speendo 2011-01-05 18:28:34

+0

關於player1和player2因獲取自己的座標造成的死鎖,不要使用synchronized和make座標volatile,應該防止死鎖,以便以下情況可以安全發生:player1試圖獲取player2的位置; player2嘗試獲取player1的位置; '以某種方式,volatile會創建一個虛構的「共享鎖」,其中多個線程可以「鎖定」到座標上,因爲該值總是在相應線程的堆棧中更新。 – 2015-08-03 15:14:27

+0

我知道這是一個古老的q/a,但是你的模型看起來像無稽之談。線程1已鎖定previousField。線程2已經鎖定nextFIeld並且正在等待當前由Thread1持有的previousField的鎖。 Thread1無法通過「鎖定下一個字段」到「刪除玩家1」。你在這裏得到的是兩個僵化的線程,而不是像你所建議的那樣「從棋盤上缺少玩家1」。 Player1根本無法刪除,因爲死鎖發生在刪除之前。 – Grod 2017-08-17 17:40:14

0

閱讀你的答案,我試着申請了如下設計:

  1. 只有鎖定的球員,而不是場
  2. 僅在synchronized方法/塊做實地操作中的一個synchronized方法/塊
  3. 總是首先檢查是否導致同步方法/塊被調用的前提條件仍然是這種情況

我認爲1.避免死鎖和3.作爲重要事物當球員等待時,球員可能會改變。

此外,我可以不鎖定字段,因爲在我的遊戲中,不止一個玩家可以停留在某個字段,只對某些線程進行交互。這種互動可以通過同步玩家來完成 - 不需要同步字段...

你覺得呢?