跨线程和逻辑的同步

Synchronization across threads and logics

我正在使用 JAVA 开发一个扑克游戏服务器,我想在我的游戏中添加 reconnect 功能。逻辑如下:

每当玩家在游戏中掉线时,其对应的数据对象将一直保留到当前游戏结束。如果他可以在游戏结束前重新连接(重新连接),他可以使用该数据对象继续游戏。

这是我的代码:

////// file: Game.java
import java.util.ArrayList;
import java.util.List;

class Game{
    private int id;

    //Maintain the list of ids of players who are playing in this game
    private List<Integer> gamePlayerIds = new ArrayList<>(9); //Max 9 players in a Poker table

    //reference back to main class
    private GameController mnt;

    public Game(int id, GameController mnt){
        this.id = id;
        this.mnt = mnt;
    }

    public void endGame(){
        //find players who are disconnected while playing and not yet reconnect
        for (Integer playerId : gamePlayerIds){
            GameController.Player player = mnt.getPlayerById(playerId);
            if (player != null && !player.isConnected()){
                //if found, remove player object from Hashmap to prevent Memory Leak
                mnt.removePlayerById(playerId);
            }
        }
    }
}

/////// file: GameController.java
import java.util.concurrent.ConcurrentHashMap;

public class GameController {

    private ConcurrentHashMap<Integer, Player> players;

    public Player getPlayerById(int id){
        return players.get(id);
    }

    public void removePlayerById(int id){
        players.remove(id);
    }

    public void addPlayer(Player player){
        players.putIfAbsent(player.getId(), player);
    }

    public GameController(){
        players = new ConcurrentHashMap<>();
        /* Do other initializations here */
    }
}

////////// file: Player.java
class Player{
    private int id;

    private boolean isConnected = true;
    private boolean isPlaying = false;

    public boolean isPlaying() {
        return isPlaying;
    }

    public void setPlaying(boolean playing) {
        isPlaying = playing;
    }

    public boolean isConnected() {
        return isConnected;
    }

    public void setConnected(boolean connected) {
        isConnected = connected;
    }

    public Player(int id){
        this.id = id;
    }

    public int getId(){
        return id;
    }
}

//////// file: OnConnectEventHandler.java
class OnConnectEventHandler {

    //reference back to main class
    GameController mnt;

    public OnConnectEventHandler(GameController mnt){
        this.mnt = mnt;
    }

    /*
    * Handle event when a connection is made. There're 3 cases:
    * 1. New connection
    * 2. Duplicated connection (already connect before, and not yet disconnect
    * 3. Reconnect
    */
    public void handleEvent(User user){

        Player player = mnt.getPlayerById(user.getId());

        if (player == null){
            //New connection, then convert User to Player and add 
            //new player object to ConcurrentHashMap which maintains the list
            //of online players
            mnt.addPlayer(new Player(user.getId()));
        } else {
            if (player.isConnected()){
                //TODO: Alert error because of duplicated login
                return;
            } else {
                //set connected flag to true, so that the endGame function
                //will not remove this reconnected user
                player.setConnected(true);
            }
        }
    }
}

///// file: OnDisconnectEventHandler 
class OnDisconnectEventHandler {

    //reference back to main class
    GameController mnt;

    public OnDisconnectEventHandler(GameController mnt){
        this.mnt = mnt;
    }

    /*
    Handle disconnect event, there are 2 cases:
    1. Disconnected player is not playing, so remove its data immediately
    2. Disconnected player is playing, so keep its data until the end of current game
       so that if he reconnect before the game ends, he can continue to play that game
    */
    public void handleEvent(User user){
        Player player = mnt.getPlayerById(user.getId());
        if (player != null){
            if (player.isPlaying()){
                //if player is disconnected while playing, just marked that he is disconnect instead of
                //removing Player object immediately
                player.setConnected(false);
            } else {
                //if player is not playing, remove Player object immediately to prevent memory leak
                mnt.removePlayerById(user.getId());
            }
        }
    }
}

我的实现在大多数情况下工作正常,但这里有一个问题: 在多线程环境下,代码可以按以下顺序执行:

我应该怎么做才能解决这个问题?

提前致谢!

更新 1 我可以像这样添加同步块吗:

//in OnConnectEventHandler class
public void handleEvent(User user){

    synchronized(mnt.players){
        Player player = mnt.getPlayerById(user.getId());

        if (player == null){
            //New connection, then convert User to Player and add 
            //new player object to ConcurrentHashMap which maintains the list
            //of online players
            mnt.addPlayer(new Player(user.getId()));
        } else {
            if (player.isConnected()){
                //TODO: Alert error because of duplicated login
                return;
            } else {
                //set connected flag to true, so that the endGame function
                //will not remove this reconnected user
                player.setConnected(true);
            }
        }
    }
}

// in OnDisconnectEventHandler class
public void handleEvent(User user){
    synchronized(mnt.players){
        Player player = mnt.getPlayerById(user.getId());
        if (player != null){
            if (player.isPlaying()){
                //if player is disconnected while playing, just marked that he is disconnect instead of
                //removing Player object immediately
                player.setConnected(false);
            } else {
                //if player is not playing, remove Player object immediately to prevent memory leak
                mnt.removePlayerById(user.getId());
            }
        }
    }
}

如果有效,是否会导致性能问题?我的游戏是多人游戏,通常有3k-5k CCU,在最拥挤的时候,每秒大约有300个connect/disconnect事件。

更新 2 我使用单独的线程来处理连接和断开连接事件。如果玩家在当前游戏结束时同时重新连接,就会出现我问题中的情况。

玩家不在游戏中时(例如在大厅,在私人房间......)可以断开连接。在那些情况下,因为玩家没有执行 "progressive" 和 "important" 操作,所以我不需要实现重新连接功能。

(1) 和 (2) 需要在 players 地图上同步,以便他们中的任何一个在获得锁定后,看到玩家地图和玩家状态的一致视图。