Java多线程井字棋同步异常:程序无法终止求助
双线程井字棋程序卡死问题排查与修复
我尝试为双线程实现的井字棋游戏做同步处理,但程序永远无法终止。每个线程对应一名Player,落子后应等待对方回合,但当前执行流程完全卡死。以下是相关代码:
TicTacToe类
public class TicTacToe { static char prevMark='O'; static char[][] board; public TicTacToe() { newBoard(); // 创建空白棋盘 } public void setMark(int x, int y, char mark) { if(x>2||y>2||(mark!='X'&&mark!='O')) // 坐标越界或标记非法则抛出异常 throw new IllegalArgumentException(); if (board[x][y]==' ') { // 空白位置才能落子 board[x][y] = mark; prevMark=mark; } else throw new IllegalArgumentException(); } public char[][] table() { return Arrays.stream(board).map(char[]::clone).toArray(char[][]::new); } public char lastMark() { return prevMark; } static private void newBoard() { board= new char[][]{{' ', ' ', ' '}, {' ', ' ', ' '}, {' ', ' ', ' '}, }; } }
该类负责棋盘创建、落子操作及记录最后落子标记以判断回合。
Player接口
public interface Player extends Runnable{ static boolean wonBoard(char[][] table, char mark){ // 检查是否有三连标记 for(int i=0; i<=2;i++){ if(table[0][i]==mark&&table[1][i]==mark&&table[2][i]==mark) return true; if(table[i][0]==mark&&table[i][1]==mark&&table[i][2]==mark) return true; } if (table[0][0] == mark && table[1][1] == mark && table[2][2] == mark) return true; return table[0][2] == mark && table[1][1] == mark && table[2][0] == mark; } static Player createPlayer(final TicTacToe ticTacToe, final char mark, PlayerStrategy strategy) { Object foo = new Object(); return new Player() { @Override public void run() { synchronized (foo) { try { do { while (mark == ticTacToe.lastMark()) { // 若最后落子是自己的标记则等待 foo.wait(); } ticTacToe.setMark(strategy.computeMove(mark, ticTacToe)[0], strategy.computeMove(mark, ticTacToe)[1], mark); // 落子 } while (!wonBoard(ticTacToe.table(), mark)); // 检查是否获胜 } catch (InterruptedException e) { e.printStackTrace(); } } } }; } }
PlayerStrategy类
public class PlayerStrategy { public int[] computeMove(final char mark, final TicTacToe ticTacToe) { final char[][] table = ticTacToe.table(); int[] coordinates = new int[0]; for (int dia = 0; dia < 5; dia++) { for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { if (i + j == dia && table[i][j] == ' ') { coordinates = new int[]{i,j}; return coordinates; } } } } throw new IllegalArgumentException(); } }
该类用于计算玩家的下一步落子坐标。
测试代码
import static org.junit.jupiter.api.Assertions.assertEquals; class GameTest { @Test void testPlayers() { testCase("" + "XOO\n" + "XXX\n" + "O ", new PlayerStrategy(), new PlayerStrategy()); } private void testCase(String expected, PlayerStrategy... strategies) { final TicTacToe ticTacToe = new TicTacToe(); final List<Thread> playerThreads = new ArrayList<>(); final List<Character> marks = Arrays.asList('X', 'O'); for (int i = 0; i < marks.size(); i++) { Player player = Player.createPlayer(ticTacToe, marks.get(i), strategies[i]); Thread thread = new Thread(player); playerThreads.add(thread); } playerThreads.forEach(Thread::start); playerThreads.forEach(silentConsumer(Thread::join)); assertEquals(expected, tableString(ticTacToe.table())); } }
工具类
import java.util.stream.Collectors; public class Utils { public static String tableString(char[][] table){ return Arrays.stream(table) .map(String::new) .collect(Collectors.joining("\n")); } }
异常处理工具
import java.util.function.Consumer; @FunctionalInterface public interface ThrowingConsumer<T, E extends Throwable> { void apply(T o) throws E; static <T, E extends Throwable> Consumer<T> silentConsumer(ThrowingConsumer<T, E> throwingConsumer) { return (param) -> { try { throwingConsumer.apply(param); } catch (Throwable e) { throw new RuntimeException(e); } }; } }
问题原因
- 锁对象不共享:每个Player线程创建时都新建独立的
Object foo作为锁,两个线程的wait()和notify()无法跨线程生效,导致线程互相等待无法唤醒。 - 缺少唤醒逻辑:落子完成后没有调用
notify()/notifyAll()唤醒对方线程,线程会一直处于等待状态。 - 静态变量线程安全问题:
TicTacToe中的prevMark和board是静态变量,多线程下读写存在可见性问题,可能导致线程读取到过期值。 - 胜利后未终止所有线程:一个玩家获胜后,另一个线程仍会等待或尝试落子,没有终止逻辑。
- 重复调用落子计算:连续两次调用
computeMove(),可能因棋盘状态变化返回不同坐标,导致setMark抛出异常中断逻辑。
修复方案
1. 共享同步锁
使用TicTacToe实例作为共享锁,确保两个线程使用同一锁对象。
2. 添加唤醒逻辑
落子后调用notifyAll()唤醒等待的对方线程。
3. 修复静态变量问题
将prevMark和board改为实例变量,添加gameOver状态标记游戏结束,并在同步方法中读写这些状态。
4. 优化落子逻辑
只调用一次computeMove()并保存结果,避免重复计算导致的坐标不一致。
修复后关键代码示例
修改后的TicTacToe类
import java.util.Arrays; public class TicTacToe { private char prevMark = 'O'; private char[][] board; private volatile boolean gameOver = false; public TicTacToe() { newBoard(); } public synchronized void setMark(int x, int y, char mark) { if (x > 2 || y > 2 || (mark != 'X' && mark != 'O')) { throw new IllegalArgumentException(); } if (board[x][y] == ' ' && !gameOver) { board[x][y] = mark; prevMark = mark; gameOver = wonBoard(board, mark); } else { throw new IllegalArgumentException(); } } public char[][] table() { synchronized (this) { return Arrays.stream(board).map(char[]::clone).toArray(char[][]::new); } } public synchronized char lastMark() { return prevMark; } public synchronized boolean isGameOver() { return gameOver; } private void newBoard() { board = new char[][]{{' ', ' ', ' '}, {' ', ' ', ' '}, {' ', ' ', ' '}}; } private boolean wonBoard(char[][] table, char mark) { for (int i = 0; i <= 2; i++) { if (table[0][i] == mark && table[1][i] == mark && table[2][i] == mark) { return true; } if (table[i][0] == mark && table[i][1] == mark && table[i][2] == mark) { return true; } } if (table[0][0] == mark && table[1][1] == mark && table[2][2] == mark) { return true; } return table[0][2] == mark && table[1][1] == mark && table[2][0] == mark; } }
修改后的Player接口
public interface Player extends Runnable { static Player createPlayer(final TicTacToe ticTacToe, final char mark, PlayerStrategy strategy) { return new Player() { @Override public void run() { synchronized (ticTacToe) { try { while (!ticTacToe.isGameOver()) { while (mark == ticTacToe.lastMark() && !ticTacToe.isGameOver()) { ticTacToe.wait(); } if (ticTacToe.isGameOver()) { break; } int[] move = strategy.computeMove(mark, ticTacToe); ticTacToe.setMark(move[0], move[1], mark); ticTacToe.notifyAll(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } }; } }
内容的提问来源于stack exchange,提问作者Piotruz
相关产品推荐
相关产品推荐

