基于JDBC的控制台游戏多线程改造与同步问题咨询
问题解答
问题1:线程调用的方法链中的方法是否属于该线程?
所有被线程调用的方法链中的方法,执行时都属于发起调用的线程。线程是一条代码执行路径,当ThreadGame的run()方法调用heroService.startGame()后,后续startGame()调用的所有子方法,只要没有手动创建新线程,都会在ThreadGame这条线程的上下文中执行,不会切换到其他线程。不存在“方法属于线程”的说法,而是方法的执行由调用它的线程负责。
问题2:synchronized应该加在何处才能解决数据存储错误的问题?
当前代码的核心问题
- 每个
Test线程都实例化了自己的OperationService,给createOperationForTest加synchronized时,锁的是各自的OperationService对象,不同线程的锁互不干扰,完全起不到互斥作用。 - 读取氏族当前金币、生成操作记录、更新金库这三个步骤没有原子性保证,并发时会出现“线程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
相关产品推荐
相关产品推荐

