You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

问题原因

  1. 锁对象不共享:每个Player线程创建时都新建独立的Object foo作为锁,两个线程的wait()和notify()无法跨线程生效,导致线程互相等待无法唤醒。
  2. 缺少唤醒逻辑:落子完成后没有调用notify()/notifyAll()唤醒对方线程,线程会一直处于等待状态。
  3. 静态变量线程安全问题:TicTacToe中的prevMark和board是静态变量,多线程下读写存在可见性问题,可能导致线程读取到过期值。
  4. 胜利后未终止所有线程:一个玩家获胜后,另一个线程仍会等待或尝试落子,没有终止逻辑。
  5. 重复调用落子计算:连续两次调用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 21:36:33