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

基于JDBC的控制台游戏多线程改造与同步问题咨询

问题解答

问题1:线程调用的方法链中的方法是否属于该线程?

所有被线程调用的方法链中的方法,执行时都属于发起调用的线程。线程是一条代码执行路径,当ThreadGame的run()方法调用heroService.startGame()后,后续startGame()调用的所有子方法,只要没有手动创建新线程,都会在ThreadGame这条线程的上下文中执行,不会切换到其他线程。不存在“方法属于线程”的说法,而是方法的执行由调用它的线程负责。

问题2:synchronized应该加在何处才能解决数据存储错误的问题?

当前代码的核心问题

  1. 每个Test线程都实例化了自己的OperationService,给createOperationForTest加synchronized时,锁的是各自的OperationService对象,不同线程的锁互不干扰,完全起不到互斥作用。
  2. 读取氏族当前金币、生成操作记录、更新金库这三个步骤没有原子性保证,并发时会出现“线程A读取金币后,线程B也读取相同值,最终两次操作只生效一次”的问题。

正确的解决方案

方案1:基于氏族的全局锁 + 数据库原子更新

要保证同一氏族的操作串行执行,同时结合数据库的原子性更新,代码调整如下:

修改OperationService类
public class OperationService {
    OperationDAO operationDAO = new OperationDAO();
    private static ClanService clanService = new ClanService();
    // 用ConcurrentHashMap存储每个氏族的专属锁,不同氏族操作互不阻塞
    private static final ConcurrentHashMap<Long, Object> CLAN_LOCKS = new ConcurrentHashMap<>();

    public void createOperationForTest(Hero hero, String reason, int sum) {
        Clan currentClan = hero.getClan();
        // 获取当前氏族对应的锁,不存在则自动创建
        Object clanLock = CLAN_LOCKS.computeIfAbsent(currentClan.getId(), k -> new Object());
        
        synchronized (clanLock) {
            // 重新从数据库读取最新的氏族数据,避免内存对象过期
            Clan freshClan = clanService.getById(currentClan.getId());
            
            // 构建操作记录
            LocalDateTime now = LocalDateTime.now();
            Operation operation = new Operation();
            operation.setClan(freshClan);
            operation.setHero(hero);
            operation.setReason(reason);
            operation.setSum(sum);
            operation.setAmountBefore(freshClan.getGold());
            operation.setAmountAfter(freshClan.getGold() + sum);
            operation.setDateTime(now);
            
            // 开启事务,保证保存操作和更新金库原子性
            try (Connection conn = getConnection()) {
                conn.setAutoCommit(false);
                operationDAO.save(conn, operation);
                clanService.updateGold(conn, freshClan.getId(), sum);
                conn.commit();
            } catch (SQLException e) {
                // 回滚事务及异常处理
                e.printStackTrace();
            }
        }
    }

    // 此处需实现获取数据库连接的方法
    private Connection getConnection() throws SQLException {
        // 示例:返回JDBC连接
        return DriverManager.getConnection("url", "user", "password");
    }
}
修改ClanService的更新方法

直接用数据库的原子更新语句,避免依赖内存对象:

public class ClanService {
    public void updateGold(Connection conn, Long clanId, int sum) throws SQLException {
        // 数据库层面的原子更新,InnoDB引擎会自动加行锁
        String sql = "UPDATE clan SET gold = gold + ? WHERE id = ?";
        try (PreparedStatement stmt = conn.prepareStatement(sql)) {
            stmt.setInt(1, sum);
            stmt.setLong(2, clanId);
            stmt.executeUpdate();
        }
    }

    // 根据ID查询氏族的方法
    public Clan getById(Long clanId) {
        // 实现JDBC查询逻辑,返回最新的氏族数据
        return null;
    }
}

关键说明

  • 锁的对象:用ConcurrentHashMap维护每个氏族的专属锁,确保同一氏族的操作串行,不同氏族可以并行,兼顾并发效率。
  • 数据库原子更新:UPDATE clan SET gold = gold + ?是数据库层面的原子操作,即使锁失效,数据库行锁也能保证更新的正确性。
  • 事务保证:将保存操作记录和更新金库放在同一事务中,避免出现“操作记录已保存但金库未更新”的不一致情况。

为什么之前的synchronized无效?

因为每个Test线程都new了独立的OperationService实例,方法上的synchronized锁的是当前实例对象,两个线程的锁完全不冲突,自然无法阻止并发问题。

内容的提问来源于stack exchange,提问作者Elisaveta Meshalkina

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 03:45:30