web-dev-qa-db-ja.com

同期ブロック-複数のオブジェクトをロックします

複数のプレイヤー(スレッド)が同時に移動するゲームをモデリングしています。プレーヤーの現在の位置に関する情報は2回保存されます。プレーヤーには、ボード上のフィールドを参照する変数「hostField」があり、すべてのフィールドには、現在このフィールドにいるプレーヤーを格納するArrayListがあります。

冗長な情報があるという事実にはあまり満足していませんが、大きなデータセットをループせずにそれを回避する方法は見つかりませんでした。

ただし、あるフィールドから別のフィールドにプレイヤーが移動する場合、(1)冗長な情報がリンクされたままである(2)現時点で他の誰もフィールドを操作していないことを確認したいと思います。

したがって、私は次のようなことをする必要があります

synchronized(player, field) {
    // code
}

どちらが不可能ですか?

私は何をすべきか? :)

38
speendo

実際、同期はオブジェクトやデータではなくコード用です。同期ブロックでパラメーターとして使用されるオブジェクト参照は、ロックを表します。

したがって、次のようなコードがある場合:

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);
    }
  }
}

その後、同期ブロックにあるにもかかわらず、ロックが同じではないため(フィールドが異なる場合)、フィールドへのアクセスは保護されません。そのため、ボードのプレーヤーのリストが不整合になり、ランタイム例外が発生する可能性があります。

代わりに、次のコードを記述すると、すべてのプレーヤーに対して共有ロックが1つしかないため、機能します。

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);
    }
  }
}

すべてのプレイヤーにアクセスするには、必ず単一のロックのみを使用してください。そうしないと、ボードの状態に一貫性がなくなります。

22

些細な解決策は

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

ただし、デッドロックを回避するために、必ず同じ順序でリソースをロックしてくださいalways

実際には、ボトルネックはフィールドであるため、フィールド上の単一のロック(または@ ripper234が正しく指摘されているように、専用の共通ロックオブジェクト)で十分な場合があることに注意してください(プレイヤーが他の競合する方法で同時に操作している場合を除きます) 。

54
Péter Török

並行性を使用する場合、適切な応答を提供することは常に困難です。それは、あなたが実際に何をしていて、何が本当に重要であるかに大きく依存しています。

私の理解では、プレーヤーの動きには以下が含まれます。

1プレーヤー位置の更新。

2前のフィールドからプレーヤーを削除します。

3新しいフィールドにプレーヤーを追加します。

同時に複数のロックを使用するが、一度に1つのロックのみを取得することを想像してください。一部のプレーヤーは、例のためにボードから姿を消したように見えることがあります。

このようにロックがロックされていると想像してください:

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は、player1がボード全体から消えたと見なします。これがアプリケーションの問題である場合、このソリューションは使用できません。

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.

=>両方のプレイヤーがブロックされています。

私の意見では、ゲームの状態全体に対して1つのロックのみを使用することが最善の解決策です。

プレイヤーが状態を読みたいときは、ゲーム全体の状態(プレイヤーとボード)をロックし、自分の使用のためにコピーを作成します。その後、ロックなしで処理できます。

プレイヤーが状態を書きたいときは、ゲーム全体の状態をロックし、新しい状態を書き、ロックを解除します。

=>ロックは、ゲーム状態の読み取り/書き込み操作に限定されます。プレイヤーは自分のコピーでボードの状態を「長時間」調べることができます。

これにより、複数のフィールドのプレイヤーのような一貫性のない状態が防止されますが、そのプレイヤーが「古い」状態を使用できるようになることは防止されません。

奇妙に見えるかもしれませんが、チェスゲームの典型的なケースです。他のプレイヤーの移動を待っているとき、移動前と同じようにボードが表示されます。他のプレイヤーがどのような動きをするかわからないので、最後に移動するまで「古い」状態で作業します。

7

モデリングについて気を悪くしないでください。これは、双方向のナビゲーション可能な関連付けにすぎません。

アトミックを操作するために(他の回答で述べたように)注意を払う場合、 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」が保護されていれば問題ありません。

「移動」セマンティクスのためにさらに原子性が必要な場合は、ボードのレベルを1つ上げます。

1
mtraut

すべての答えを読んで、次のデザインを適用しようとしました。

  1. フィールドではなくプレイヤーのみをロックする
  2. 同期されたメソッド/ブロックでのみフィールド操作を実行します
  3. 同期化されたメソッド/ブロックでは、常に、同期化されたメソッド/ブロックが呼び出される原因となった前提条件がまだ当てはまるかどうかを最初にチェックします

1.デッドロックを回避し、3。プレイヤーが待機している間に状況が変化する可能性があるため重要です。

さらに、ゲームでは複数のプレイヤーがフィールドに留まることができるので、フィールドをロックせずに進むことができます。特定のスレッドについてのみ相互作用を行う必要があります。この相互作用は、プレーヤーを同期することで実行できます-フィールドを同期する必要はありません...

どう思いますか?

0
speendo