Подтвердить что ты не робот

Синхронизировать блокировку нескольких объектов

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

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

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

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

synchronized(player, field) {
    // code
}

Что не возможно, правильно?

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

4b9b3361

Ответ 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 = 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 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);
    }
  }
}

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

Ответ 2

Тривиальное решение было бы

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

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

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

Ответ 3

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

По моему мнению, движение игрока включает:

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 : 

Thread 2 считает, что плеер1 исчез из всей доски. Если это проблема для вашего приложения, вы не можете использовать это решение.

Вспомогательная проблема для фиксированной блокировки: потоки могут застревать. Представьте 2 игрока: они обмениваются своей позицией в одно и то же время:

player1 aquire it own position at the same time
player2 aquire it 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.

= > Оба игрока заблокированы.

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

Когда игрок хочет прочитать состояние, он блокирует все состояние игры (игроки и доски) и создает копию для собственного использования. Затем он может обрабатываться без блокировки.

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

= > Блокировка ограничена операциями чтения/записи состояния игры. Игрок может выполнять "длительное" рассмотрение состояния платы на своей собственной копии.

Это предотвращает любое несогласованное состояние, такое как игрок в нескольких полевых или ни одного, но не мешает игроку использовать "старое" состояние.

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

Ответ 4

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

Если вы позаботитесь (как в других ответах), чтобы манипулировать атомами, например, в методах 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" был защищен.

Если вам нужна дополнительная атомарность для семантики перемещения, перейдите на один уровень вверх для доски.

Ответ 5

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

  • Блокировать игроков, а не поля
  • Выполнять полевые операции только в синхронизированных методах/блоках
  • в синхронизированном методе/блоке всегда сначала проверяют, являются ли предпосылки, вызвавшие вызванный синхронный метод/блок, все еще.

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

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

Как вы думаете?