Java 网络swing游戏中的多线程:使用invokeLater vs locks
我正在编写一个简单的自上而下的太空游戏,并将其扩展到允许多人通过网络进行游戏。我已经读了不少书,但这是我第一次这样做,我希望能得到一些关于选择合理设计的建议 我的GUI是使用Swing编写的。每秒30次,计时器触发,并根据内存中游戏世界对象中的数据重新绘制GUI(基本上是带有位置的舰船和投射物列表等)。游戏世界的物理更新也使用这个计时器进行。因此,对于单机版实现,所有事情都发生在EDT上,这很好 现在,我有单独的线程处理来自其他玩家的传入数据包。我想根据这些数据包包含的内容更新游戏世界对象中的数据。我的问题是,我应该使用invokeLater来进行这些更改,还是应该使用锁来避免并发问题 为了说明我的意思:Java 网络swing游戏中的多线程:使用invokeLater vs locks,java,multithreading,swing,networking,Java,Multithreading,Swing,Networking,我正在编写一个简单的自上而下的太空游戏,并将其扩展到允许多人通过网络进行游戏。我已经读了不少书,但这是我第一次这样做,我希望能得到一些关于选择合理设计的建议 我的GUI是使用Swing编写的。每秒30次,计时器触发,并根据内存中游戏世界对象中的数据重新绘制GUI(基本上是带有位置的舰船和投射物列表等)。游戏世界的物理更新也使用这个计时器进行。因此,对于单机版实现,所有事情都发生在EDT上,这很好 现在,我有单独的线程处理来自其他玩家的传入数据包。我想根据这些数据包包含的内容更新游戏世界对象中的数
runMethodOfInputThread() {
while(takingInput) {
data = receiveAndInterpretIncomingPacket(); // blocks
SwingUtilities.invokeLater(new Runnable() {
public void run() {
gameWorld.updateWithNewGameInfo(data);
}
});
}
}
vs
后者还需要在EDT访问游戏世界的任何地方使用类似的同步块,因此在我看来,使用invokeLater更容易实现。但我认为这两种方法都有效,对吗?还有其他重要的优点/缺点需要牢记吗
谢谢
Jeremy首先,你不需要只选择一种方法。您可以使用锁“只是为了确保”数据结构是线程安全的(因为您的应用程序已经是多线程的),并使用invokeLater仅在EDT中应用更改——在这种情况下,JIT可能会优化您的锁,接近0 接下来,在我看来,invokeLater是一种更可取的方式:如果您可以绕过多线程处理——您应该使用这种方式,因为多线程处理很难,而且可能会有很多错误 但通过invokeLater()应用更改将给EDT带来额外的压力,因此,如果更改速度很快,您可以观察到GUI降级。另外,如果gameWorld.updateWithNewGameInfo(数据)是一种花费可观察时间来完成的方法,那么它甚至会使您的GUI冻结。另外,invokeLater将您的任务放在事件队列的末尾,所以它将在队列中当前的所有事件之后完成。在某些情况下,它可能会导致应用更改的延迟,这会使您的游戏更不友好。这可能是,也可能不是你的情况,但你应该记住这一点
至于一般规则——不要将EDT用于任何耗时的任务。据我所知,在应用程序中,网络数据包解析已经在单独的线程中。应用更改也可以(而且应该)在单独的线程中完成,如果这很耗时的话。不是第二个线程。您希望在EDT中运行的时间尽可能少。如果您正在等待EDT中的锁,这与直接在EDT中运行所有其他代码(在锁的另一侧)一样糟糕,因为EDT必须等待其他所有代码完成 而且,你的整个游戏似乎都在EDT上运行。那是个坏习惯。您应该使用模型-视图-控制器模式拆分代码。我知道你的游戏很小,可以在EDT中运行,但你可能不应该养成这种习惯 您应该让游戏逻辑从计时器线程(java.util.concurrent.ScheduledThreadPoolExecutor)运行,并在每个周期结束时将数据“发送”到EDT,然后使用
调用器重新绘制
您还应该有一些单独的线程来读取套接字,该线程应该写入与计时器游戏线程中使用的对象共享锁的对象 方法1的优点:
- 最小化复杂性
- 稳定性
通过限制EDT线程对“gameWorld”变量的访问,不需要锁定机制。并发编程非常复杂,需要程序员在访问线程之间共享的对象时在整个源代码库中保持警惕。有可能
程序员在某些情况下忘记同步,导致游戏状态受损或程序失败
方法2的优点:
- 可伸缩性
- 演出
最小化EDT线程上的处理可确保游戏界面和显示对用户保持响应。方法1目前可能有效,但如果EDT线程忙于进行非ui处理,则游戏的后续版本将无法扩展到更高级的界面。我的建议如下
- 将从不同用户(线程)加载的所有数据推送到队列
- 使用另一个线程从该队列读取并从EDT更新UI
它应该可以避免并发问题。如何实现
runMethodOfInputThread() {
while(takingInput) {
data = receiveAndInterpretIncomingPacket(); // blocks
blockingQueue.add(data);
}
}
runMethodOfUPdateUIThread() {
while(updatingUI) {
data = blockingQueue.take();
SwingUtilities.invokeLater(new Runnable() {
public void run() {
gameWorld.updateWithNewGameInfo(data);
}
});
}
}
经验法则:在EDT中尽量少做,总是在EDT中更新组件,并尝试选择invokeLater()而不是invokeAndWait()。非常感谢您所有经过深思熟虑的回复,您给了我很多思考。我想这一条总结了我最想要的。我想我现在会选择第一种。在输入线程gameWorld上解析网络数据包。updateWithNewGameInfo(data)
可能不会特别繁重(它真正做的只是在gameWorld中设置一些变量)。如果事情开始陷入困境,或者我决定支持数以百万计的球员,我会重新考虑。
runMethodOfInputThread() {
while(takingInput) {
data = receiveAndInterpretIncomingPacket(); // blocks
blockingQueue.add(data);
}
}
runMethodOfUPdateUIThread() {
while(updatingUI) {
data = blockingQueue.take();
SwingUtilities.invokeLater(new Runnable() {
public void run() {
gameWorld.updateWithNewGameInfo(data);
}
});
}
}