关于LeetCode按序打印问题的多线程可见性疑问
LeetCode 按序打印问题的Java解法与疑问解答
我正在解决LeetCode的按序打印问题,尝试了三种Java解决方案:
解决方案1(同步方法+wait/notify)
// Solution 1: // 25 ms time // The first solution is slower? Probably because waking up a thread is more costly. The second solution is more close to positive lock. class Foo { int lockSecond = 0; int lockThird = 0; public Foo() { } public synchronized void first(Runnable printFirst) throws InterruptedException { // printFirst.run() outputs "first". Do not change or remove this line. printFirst.run(); this.lockSecond = 1; notifyAll(); } public synchronized void second(Runnable printSecond) throws InterruptedException { for (;this.lockSecond == 0;) { wait(); } // printSecond.run() outputs "second". Do not change or remove this line. printSecond.run(); this.lockThird = 1; notifyAll(); } public synchronized void third(Runnable printThird) throws InterruptedException { for (;this.lockThird == 0;) { wait(); } // printThird.run() outputs "third". Do not change or remove this line. printThird.run(); notifyAll(); } }
解决方案2(轮询+Thread.sleep)
//Solution 2: //10 ms there is no valotile, which means thread will retieve latest value when finishing sleeping class Foo { int lockSecond = 0; int lockThird = 0; public Foo() { } public void first(Runnable printFirst) throws InterruptedException { // printFirst.run() outputs "first". Do not change or remove this line. printFirst.run(); this.lockSecond = 1; } public void second(Runnable printSecond) throws InterruptedException { for (;this.lockSecond == 0;) { Thread.sleep(1); } // printSecond.run() outputs "second". Do not change or remove this line. printSecond.run(); this.lockThird = 1; } public void third(Runnable printThird) throws InterruptedException { for (;this.lockThird==0;) { Thread.sleep(1); } // printThird.run() outputs "third". Do not change or remove this line. printThird.run(); } }
解决方案3(volatile+空轮询)
// same as second solution, //Solution 3: class Foo { volatile int lockSecond = 0; volatile int lockThird = 0; public Foo() { } public void first(Runnable printFirst) throws InterruptedException { // printFirst.run() outputs "first". Do not change or remove this line. printFirst.run(); this.lockSecond = 1; } public void second(Runnable printSecond) throws InterruptedException { for (;this.lockSecond == 0;) { } // printSecond.run() outputs "second". Do not change or remove this line. printSecond.run(); this.lockThird = 1; } public void third(Runnable printThird) throws InterruptedException { for (;this.lockThird==0;) { } // printThird.run() outputs "third". Do not change or remove this line. printThird.run(); } }
疑问点
我理解方案3中使用volatile关键字确保变量在多线程间的可见性,从而避免死循环;若移除volatile,LeetCode会提示超时。但方案2中不使用volatile,仅在循环内加入Thread.sleep(1)就能通过测试,我原本以为这是因为线程休眠结束后会重读变量,但后续发现这和sleep无关,只要循环体中有语句(比如替换为println)就能通过测试。我原本认为无volatile时会因线程间无可见性陷入死循环,但实际只是变慢,有语句时甚至能通过测试,对此存在疑问。
问题解答
这个现象的核心原因是JIT即时编译器的优化行为以及Java内存模型的可见性规则:
空循环的JIT优化陷阱
当循环体为空时,JIT编译器会做激进优化:它会判断线程在循环中没有修改lockSecond/lockThird的值,于是将变量从主内存缓存到线程的寄存器或本地栈中,后续循环只会读取本地缓存的值,不会再去主内存同步最新值。即使其他线程修改了主内存中的变量值,当前线程也看不到,最终导致无限循环,触发LeetCode的超时判定。循环体有语句时的内存行为
当循环体中加入Thread.sleep(1)、System.out.println()这类语句时,JIT不会对该循环做上述激进优化:
- 像
Thread.sleep()这类阻塞方法,线程从休眠状态恢复后,JVM会保证该线程重新读取主内存中的变量值,而非使用本地缓存; - 即使是普通语句,JIT也会因为循环体存在额外操作,放弃将变量缓存到寄存器的优化,线程会定期去主内存同步变量值,因此能看到其他线程修改后的
lockSecond/lockThird,从而退出循环。这种情况虽然没有volatile保证严格的可见性,但在LeetCode的测试环境下,线程有足够的机会读取到最新值,因此能通过测试,只是轮询等待会消耗更多CPU资源,整体运行速度较慢。
- volatile的作用
volatile关键字的核心作用是:
- 禁止JIT对变量做缓存优化,强制线程每次读取变量都直接从主内存获取;
- 禁止指令重排,保证变量修改的顺序性。
因此方案3中使用volatile后,即使是空循环,线程也会持续读取主内存中的最新值,一旦其他线程修改变量,当前线程能立即感知,从而高效退出循环。
内容的提问来源于stack exchange,提问作者Stan
相关产品推荐
相关产品推荐

