同步块——锁定多个对象

发布于 2024-10-10 14:32:16 字数 356 浏览 0 评论 0原文

我正在建模一个游戏,其中多个玩家(线程)同时移动。 玩家当前所在位置的信息被存储两次:玩家有一个变量“hostField”,它引用棋盘上的一个字段,每个字段都有一个 ArrayList 存储当前位于该字段的玩家。

我对拥有冗余信息这一事实不太满意,但我发现如果不循环遍历大数据集,就无法避免这种情况。

然而,当一名玩家从一个场地移动到另一个场地时,我想确保(1)冗余信息保持链接(2)目前没有其他人操纵该场地。

因此我需要做类似的事情

synchronized(player, field) {
    // code
}

这是不可能的,对吧?

我应该怎么办? :)

I'm modelling a game where multiple players (threads) move at the same time.
The information of where a player is located at the moment is stored twice: the player has a variable "hostField" that references to a field on the board and every field has an ArrayList storing the players that are currently located at this field.

I'm not very happy with the fact that I have redundant information, but I found no way avoiding that without looping through a big dataset.

However, when a player moves from one field to another, I'd like to make sure (1) the redundant information stays linked (2) nobody else is manipulating the field at the moment.

Therefore I need to do something like

synchronized(player, field) {
    // code
}

Which is not possible, right?

What should I do? :)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

ㄟ。诗瑗 2024-10-17 14:32:16

一个简单的解决方案是

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

但是,请确保您始终以相同的顺序锁定资源以避免死锁。

请注意,在实践中,瓶颈是字段,因此字段上的单个锁定(或专用的公共锁定对象,正如 @ripper234 正确指出的那样)可能就足够了(除非您同时以其他冲突的方式操纵玩家) 。

A trivial solution would be

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

However, make sure that you always lock the resources in the same order to avoid deadlocks.

Note that in practice, the bottleneck is the field, so a single lock on the field (or on a dedicated, common lock object, as @ripper234 rightly pointed out) may suffice (unless you are concurrently manipulating players in other, conflicting ways).

酒浓于脸红 2024-10-17 14:32:16

事实上,同步是针对代码的,而不是针对对象或数据的。在同步块中用作参数的对象引用代表锁。

因此,如果您有如下代码:

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

请务必仅使用一个锁来访问所有玩家,否则您的棋盘状态将不一致。

In fact, synchronization is for code, not objects or data. The object reference used as a parameter in synchronized block represent the lock.

So if you have code like:

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

Then Despite being in the synchronized block the access to field is not protected because the lock is not the same (it being on different instances). So your List of Players for your board can become inconsistent and cause runtime exceptions.

Instead if you write the following code, it will work because we have only one shared lock for all players:

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

Be sure to use only a single lock to access all the players or your board's state will be inconsistent.

北陌 2024-10-17 14:32:16

使用并发时,总是很难给出好的响应。这很大程度上取决于您真正在做什么以及真正重要的事情。

根据我的理解,玩家移动涉及:

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 认为玩家 1 从整个棋盘上消失了。如果这样是您的应用程序的问题,您不能使用此解决方案。

临时锁定的另一个问题是:螺纹可能会被卡住。
想象一下 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.

=>两名球员均被封锁。

我认为最好的解决方案是对游戏的整个状态仅使用一把锁。

当玩家想要读取状态时,它会锁定整个游戏状态(玩家和棋盘),并制作副本供自己使用。然后它就可以在没有任何锁的情况下进行处理。

当玩家想要写入状态时,它会锁定整个游戏状态,写入新状态,然后释放锁定。

=>锁定仅限于游戏状态的读/写操作。玩家可以在自己的副本上对棋盘状态进行“长时间”检查。

这可以防止任何不一致的状态,例如玩家在多个字段中或没有,但不会阻止该玩家可以使用“旧”状态。

它可能看起来很奇怪,但这是国际象棋游戏的典型情况。当您等待其他玩家移动时,您看到的棋盘与移动之前一样。您不知道其他玩家会采取什么行动,直到他最终移动为止,您都在“旧”状态下工作。

When using concurrency, it is always difficult to give good responses. It highly depends of what your are really doing and what really matter.

From my understanding, a player move involve :

1 Updading player position.

2 Removing the player from previous field.

3 Adding player to new field.

Imagine you use several locks at the same time but aquire only one at a time :
- Another player can perfectly look at the wrong moment, basically between 1&2 or 2&3. Some player can appear to have disapeared from the board for exemple.

Imagine your do imbricked locking like this :

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

The problem is... It don't work, see this order of execution for 2 threads :

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 think that player1 as disapeared from the whole board. If this is a problem for your application, you can't use this solution.

Additionnal problem for imbriqued locking : threads can become stuck.
Imagine 2 players : they exchange their position at exactly the same time :

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.

=> Both players are blocked.

Best solution in my opinion is to use only one lock, for the whole state of the game.

When a player want to read the state, it lock the whole game state (players & board), and make a copy for its own usage. It can then process without any lock.

When a player want to write the state, it lock the whole game state, write the new state and then release the lock.

=> Lock is limited to read/write operations of the game state. Player can perform "long" examination of the board state on their own copy.

This prevent any inconsistant state, like a player in several field or none, but don't prevent that player can use "old" state.

It can appear weird, but it is the typical case of a chess game. When you are waiting for the other player to move, you see the board as before the move. You don't know what move the other player will make and until he has finally moved, you work on an "old" state.

失与倦" 2024-10-17 14:32:16

您不应该对您的建模感到难过 - 这只是一个双向可导航的关联。

如果您小心(如其他答案中所述)操作原子,例如在 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”受到保护就好了。

如果您需要“移动”语义的进一步原子性,请为板升一级。

You should not feel bad about your modelling - this is only a two way navigable association.

If you take care (as in the other answers told) to manipulate atomic, e.g. in the Field methods, thats fine.


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


It would be fine if "Player.setField" were protected.

If you need further atomicity for "move" semantics, go one level up for board.

月亮是我掰弯的 2024-10-17 14:32:16

阅读您的所有答案,我尝试应用以下设计:

  1. 仅锁定玩家,而不是字段
  2. 仅在同步方法/块中的同步方法/块中进行字段操作
  3. 始终首先检查是否导致调用同步方法/块的前提条件 我认为情况仍然如此:

1.避免僵局,3.很重要,因为当玩家等待时事情可能会发生变化。

此外,我可以在不锁定字段的情况下进行,因为在我的游戏中,多个玩家可以留在一个字段中,只有某些线程必须进行交互。这种交互可以通过同步玩家来完成——不需要同步字段......

你觉得怎么样?

Reading all your answers, I tried to apply the following design:

  1. Only lock players, not fields
  2. Do field operations only in synchronized methods/blocks
  3. in a synchronized method/block always first check if the preconditions that caused the synchronized method/block to be called are still the case

I think 1. avoids deadlocks and 3. is important as things could change while a player waits.

Further I can go without locking fields as in my game more than one player can stay at a field, only for certain threads an interaction has to be done. This interaction can be done by synchronising players - no need to sync fields...

What do you think?

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文