我正在为多个玩家(线程)同时移动的游戏建模。 此时玩家所处位置的信息被存储两次:玩家有一个变量“hostField”,它引用棋盘上的一个字段,每个字段都有一个ArrayList,用于存储当前位于该字段的玩家。
我对我有冗余信息这一事实并不十分满意,但我发现如果不循环使用大数据集,我就无法避免这种情况。
然而,当玩家从一个领域移动到另一个领域时,我想确保(1)冗余信息保持联系(2)此刻没有其他人在操纵该领域。
因此我需要做一些像
这样的事情synchronized(player, field) {
// code
}
哪种情况不可能,对吧?
我该怎么办? :)
答案 0 :(得分:53)
一个简单的解决方案是
synchronized(player) {
synchronized(field) {
// code
}
}
但是,请确保始终以相同顺序锁定资源以避免死锁。
请注意,在实践中,瓶颈是字段,因此字段上的单个锁定(或专用的公共锁定对象,如@ ripper234正确指出的)可能就足够了(除非您同时操纵其他的玩家,相互冲突的方式)。
答案 1 :(得分:21)
实际上,同步是针对代码,而不是对象或数据。用作synchronized块中的参数的对象引用表示锁定。
所以如果您有以下代码:
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);
}
}
}
请务必仅使用一个锁来访问所有玩家,否则您的棋盘状态将会不一致。
答案 2 :(得分:7)
使用并发时,总是很难给出好的回复。这在很大程度上取决于你的确在做什么以及真正重要的事情。
根据我的理解,球员移动涉及:
1个更新玩家位置。
2从前一个字段中删除播放器。
3将玩家添加到新领域。
想象一下,您同时使用多个锁,但一次只能获取一个: - 另一个玩家可以完美地看到错误的时刻,基本上在1&amp; 2或2&amp; 3之间。例如,有些玩家似乎已经从董事会中消失了。
想象一下,你这样做是一种错误的锁定:
synchronized(player) {
synchronized(previousField) {
synchronized(nextField) {
...
}
}
}
问题是......它不起作用,请看两个线程的执行顺序:
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.
=&GT;两名球员都被封锁了。
在我看来,最佳解决方案是只使用一个锁,用于整个游戏状态。
当玩家想要阅读状态时,它会锁定整个游戏状态(玩家和玩家),并为自己的使用制作副本。然后它可以在没有任何锁定的情况下处理。
当玩家想要写入状态时,它会锁定整个游戏状态,写入新状态然后释放锁定。
=&GT;锁定仅限于游戏状态的读/写操作。玩家可以在自己的副本上对董事会状态进行“长期”检查。
这可以防止任何不一致的状态,比如几个领域的玩家或者没有,但是不要阻止玩家使用“旧”状态。
它可能看起来很奇怪,但它是象棋游戏的典型案例。当您等待其他玩家移动时,您会看到移动前的棋盘。你不知道其他玩家会做出什么动作,直到他最终移动,你才会处于“旧”状态。
答案 3 :(得分: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”受到保护,那就没问题了。
如果你需要进一步使用“移动”语义的原子性,那就去上一层了。
答案 4 :(得分:0)
阅读所有答案,我尝试应用以下设计:
我认为1.避免死锁和3.重要的是因为玩家等待时事情会发生变化。
此外,我可以不使用锁定字段,因为在我的游戏中,不止一个玩家可以留在一个字段中,只有某些线程才能进行交互。这种互动可以通过同步玩家来完成 - 无需同步字段......
您怎么看?