синхронизированный блок - блокировка нескольких объектов - PullRequest
37 голосов
/ 05 января 2011

Я моделирую игру, в которой несколько игроков (потоков) перемещаются одновременно.Информация о том, где в данный момент находится игрок, сохраняется дважды: у игрока есть переменная «hostField», которая ссылается на поле на доске, а каждое поле имеет ArrayList, хранящий игроков, которые в данный момент находятся на этом поле.

Я не очень доволен тем фактом, что у меня есть избыточная информация, но я не нашел способа избежать этого, не просматривая большой набор данных.

Однако, когда игрок перемещается с одного поля на другоеЯ хотел бы убедиться, что (1) избыточная информация остается связанной (2) никто больше не манипулирует полем в данный момент.

Поэтому мне нужно сделать что-то вроде

synchronized(player, field) {
    // code
}

Что невозможно, верно?

Что мне делать?:)

Ответы [ 5 ]

53 голосов
/ 05 января 2011

Тривиальным решением будет:

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

Однако убедитесь, что вы всегда блокируете ресурсы в одном и том же порядке, чтобы избежать взаимных блокировок.На практике узким местом является поле, поэтому может быть достаточно одной блокировки на поле (или на выделенном общем объекте блокировки, как правильно заметил @ ripper234) (если вы одновременно не манипулируете игроками другими, конфликтующими способами).*

21 голосов
/ 05 января 2011

На самом деле синхронизация для кода, а не для объектов или данных.Ссылка на объект, используемая в качестве параметра в синхронизированном блоке, представляет блокировку.

Таким образом, если у вас есть такой код:

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

Тогда, несмотря на то, что в синхронизированном блоке доступ к полю не защищенпотому что блокировка не одинакова (это происходит в разных случаях).Таким образом, ваш Список игроков для вашей доски может стать непоследовательным и вызывать исключения во время выполнения.

Вместо этого, если вы напишите следующий код, он будет работать, потому что у нас есть только одна общая блокировка для всех игроков:

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

Обязательно используйте только один замок для доступа ко всем игрокам, иначе состояние вашей доски будет несовместимым.

7 голосов
/ 05 января 2011

При использовании параллелизма всегда сложно дать хорошие ответы.Это сильно зависит от того, что вы действительно делаете и что действительно важно.

Насколько я понимаю, ход игрока включает в себя:

1 Обновление позиции игрока.

2 Удаление игрокас предыдущего поля.

3 Добавление игрока на новое поле.

Представьте, что вы используете несколько замков одновременно, но приобретаете только по одному за раз: - Другой игрок может отлично смотреть в неподходящий моментв основном между 1 и 2 или 2 и 3.Может показаться, что некоторые игроки исчезли с игрового поля в качестве примера.

Представьте, что вы заблокировали блокировку так:

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 исчез из всей доски. Если это проблема для вашего приложения, вы не можете использовать это решение.

Дополнительная проблема для блокировки блокировки: резьба может застрять.Представьте себе 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 голос
/ 05 января 2011

Вы не должны расстраиваться из-за своего моделирования - это только двусторонняя судоходная ассоциация.

Если вы позаботитесь (как и в других ответах), чтобы манипулировать атомами, например, в полевых методах это нормально.


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 голосов
/ 05 января 2011

Читая все ваши ответы, я попытался применить следующий дизайн:

  1. Только блокировка игроков, а не полей
  2. Выполнять полевые операции только в синхронизированных методах / блоках
  3. в синхронизированном методе / блоке всегда в первую очередь проверяют, сохраняются ли предусловия, вызвавшие вызов синхронизированного метода / блока,

Я думаю, что 1. избегает тупиков и 3. важно, поскольку все может измениться, пока игрок ждет.

Кроме того, я могу обходиться без блокировки полей, так как в моей игре более одного игрока могут оставаться на поле, только для определенных потоков взаимодействие должно быть выполнено. Это взаимодействие может быть выполнено путем синхронизации игроков - нет необходимости синхронизировать поля ...

Что вы думаете?

...