多线程共享对象布尔变量同步问题: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
相关产品推荐
相关产品推荐

