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

多线程共享对象布尔变量同步问题:println为何能修复死循环?

问题描述

我开发的程序基于两个共享GameProtocol对象的线程,原本意图让一个线程等待另一个线程第二次调用update方法后再执行。但实际运行时,第二个线程会卡在while循环中,即便waitForCategory布尔变量已被设为false。奇怪的是,添加System.out.println(protocol.waitForCategory)语句后程序恢复正常,移除该语句又会失效。我无法理解该打印语句的作用,希望找到更规范的替代解决方法。


相关代码

Main类

GameProtocol protocol = new GameProtocol();
MyThreadedClass thread1 = new MyThreadedClass(protocol);
MyThreadedClass thread2 = new MyThreadedClass(protocol);
thread1.start();
thread2.start();

线程类(MyThreadedClass)

GameProtocol protocol;

private MyThreadedClass(GameProtocol protocol){
   this.protocol = protocol;
}

private GamePackage waitCheck(GamePackage gp){
   if(!gp.isWaiting()) {
      return protocol.update(gp);
   }
   while(protocol.waitForCategory) {
      //System.out.println(protocol.waitForCategory);
   }
   return protocol.update(gp);
}

GameProtocol类

boolean waitForCategory = false;

public synchronized GamePackage update(GamePackage gp){

   if(gp.myTurnToPickCategory){
      gp.setWaiting(false);
      waitForCategory = true;
   } else {
     gp.setWaiting(true);
     waitForCategory = false;
   }
   return gp;
}

问题根源

这是线程可见性问题导致的:

  • waitForCategory是普通布尔变量,线程读取它时可能直接从本地缓存取值,而非主存。当另一个线程修改该变量后,当前线程无法感知到主存中的最新值,因此会一直卡在循环里。
  • System.out.println()方法内部包含synchronized同步块,执行时会强制线程刷新主存,让当前线程能读到变量的最新值——这只是该方法的副作用,并非正确的解决手段。

规范解决方案

方案1:用volatile修饰变量

给GameProtocol中的waitForCategory加上volatile关键字,强制线程每次从主存读取变量值,修改后立即同步到主存,保证线程间的可见性:

volatile boolean waitForCategory = false;

方案2:用synchronized包裹循环读取

在读取waitForCategory时,用synchronized锁住protocol对象,利用synchronized的主存同步特性保证可见性:

while(protocol.waitForCategory) {
    synchronized(protocol) {
        // 空循环,通过同步保证可见性
    }
}

注:此方式效率略低,因为每次循环都需加锁解锁。

方案3:使用Java并发工具类(推荐)

如果是等待特定次数方法调用的场景,使用CountDownLatch这类并发工具类语义更清晰,也更符合最佳实践:

修改GameProtocol类

import java.util.concurrent.CountDownLatch;

class GameProtocol {
    private final CountDownLatch updateLatch = new CountDownLatch(2); // 计数为2,对应两次update调用

    public synchronized GamePackage update(GamePackage gp){
        if(gp.myTurnToPickCategory){
            gp.setWaiting(false);
        } else {
            gp.setWaiting(true);
        }
        updateLatch.countDown(); // 每次调用update,计数减1
        return gp;
    }

    public void waitForSecondUpdate() throws InterruptedException {
        updateLatch.await(); // 等待计数归零,即两次update完成
    }
}

修改线程类的waitCheck方法

private GamePackage waitCheck(GamePackage gp){
    if(!gp.isWaiting()) {
        return protocol.update(gp);
    }
    try {
        protocol.waitForSecondUpdate();
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt(); // 恢复线程中断状态
    }
    return protocol.update(gp);
}

这种方式避免了空循环的资源浪费,同时明确表达了“等待两次update调用”的业务意图。


内容的提问来源于stack exchange,提问作者Gustav Henriksson

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 00:01:14