Java线程实现简易Ludo游戏的同步及报错问题排查
问题根因
你遇到的current thread is not owner(当前线程不是锁持有者)错误,是Java并发编程的常见基础错误:wait()、notify()、notifyAll()三个方法必须由持有目标对象监视器锁的线程调用,你的代码里两个核心设计缺陷直接触发了这个报错,同时也不满足线程同步的要求:
- 你在
finishTurn()方法中直接调用下一个Player对象的notify(),但执行该方法的当前回合线程,根本没有持有下一个Player对象的锁,直接触发非法监视器状态异常。 - 每个Player线程在自身对象实例上加锁等待,锁资源是分散的,根本无法实现“同一时间仅一个线程操作共享游戏数据”的要求,多线程并发读写棋盘、玩家状态时必然出现数据错乱。
线程创建位置说明
你在Game.run()方法中创建并启动所有玩家线程的位置是合理的:Game作为全局游戏调度类,持有所有玩家、棋盘数据的引用,在这里统一管理线程生命周期符合职责划分。需要注意的是,启动所有线程后不要立刻触发回合切换,要等所有玩家线程都进入等待状态后再发出第一个回合信号,避免信号丢失导致线程永久阻塞。
修正后实现
核心调整思路:使用Game类实例作为全局唯一共享锁,所有玩家线程都竞争这把锁,所有对共享游戏数据的读写、wait/notify操作都基于这把锁执行,既解决锁持有者异常,也天然保证同一时间只有一个线程操作共享数据。
修正后的Player.java
public class Player implements Runnable { String name; Game game; // 回合标记,避免虚假唤醒 private boolean isMyTurn = false; public Player(String name, Game game) { this.name = name; this.game = game; } public void setMyTurn(boolean myTurn) { isMyTurn = myTurn; } @Override public void run() { // 全局竞争Game实例锁,保证同一时间仅一个线程执行游戏逻辑 synchronized (game) { while (!Thread.currentThread().isInterrupted()) { try { // 未轮到自己则持续等待 while (!isMyTurn) { game.wait(); } // 执行回合逻辑 System.out.println(name + " 的回合开始"); // 此处编写掷骰子、移动棋子、碰撞判定等具体游戏逻辑 // 回合结束,重置自身标记,触发下一个玩家回合 isMyTurn = false; game.finishTurn(this); // 产生获胜者后退出线程循环 if (game.getWinner() != null) { break; } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); } } } } @Override public String toString() { return name; } }
修正后的Game.java
public class Game implements Runnable { private List<Player> players; private List<Player> fields; private int players_finished; private Player winner; public Game() { this.players = new ArrayList<>(); this.fields = new ArrayList<>(); this.players_finished = 0; this.winner = null; } public Player getWinner() { return winner; } public void init() { System.out.println("游戏启动"); players.add(new Player("Magnum", this)); players.add(new Player("Felix", this)); players.add(new Player("Alex", this)); players.add(new Player("Sandra", this)); for(int i = 0; i < 20; i++) { fields.add(i, null); } for(int i = 0; i < 20; i++) { // 每隔5格放置玩家初始棋子 if(i % 5 == 0) { fields.set(i, players.get((int)i / 5)); } } } @Override public void run() { List<Thread> playerThreads = new ArrayList<>(); for(Player player : players) { Thread t = new Thread(player); playerThreads.add(t); t.start(); } // 等待所有玩家线程进入等待状态,避免启动信号丢失 try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // 持有全局锁后通知第一个玩家开始回合 synchronized (this) { players.get(0).setMyTurn(true); notifyAll(); } // 等待所有玩家线程执行完毕 for(Thread t : playerThreads) { try { t.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } System.out.println("游戏结束,获胜者为:" + winner); } void finishTurn(Player player) { System.out.println(player + " 完成回合"); int currentIndex = players.indexOf(player); // 此处可添加玩家完赛判定逻辑:如果当前玩家所有棋子到达终点,players_finished计数+1,满足获胜条件则设置winner // 计算下一个玩家索引,循环轮转 int nextIndex = (currentIndex + 1) % players.size(); // 可添加逻辑跳过已完赛的玩家 Player nextPlayer = players.get(nextIndex); nextPlayer.setMyTurn(true); // 唤醒所有等待在全局锁上的线程,仅标记为isMyTurn的玩家会执行逻辑 notifyAll(); } }
Main.java 无需修改,保持原有逻辑即可
public class Main { public static void main(String[] args) { Game game = new Game(); game.init(); game.run(); } }
关键注意事项
- 所有
wait()调用必须放在循环条件判断中,不要用if单次判断,避免JVM的虚假唤醒机制导致逻辑错乱。 - 所有对玩家列表、棋盘格子、获胜状态等共享数据的读写操作,必须放在持有Game全局锁的代码块中执行,从根源上避免并发数据不一致问题。
- 不要使用单个Player实例作为锁对象,否则调用notify时必须持有对应Player的锁,不仅编码容易出错,也无法实现全局的互斥访问控制。
内容的提问来源于stack exchange,提问作者Deutrys
相关产品推荐
相关产品推荐

