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

关于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内存模型的可见性规则:

  1. 空循环的JIT优化陷阱
    当循环体为空时,JIT编译器会做激进优化:它会判断线程在循环中没有修改lockSecond/lockThird的值,于是将变量从主内存缓存到线程的寄存器或本地栈中,后续循环只会读取本地缓存的值,不会再去主内存同步最新值。即使其他线程修改了主内存中的变量值,当前线程也看不到,最终导致无限循环,触发LeetCode的超时判定。

  2. 循环体有语句时的内存行为
    当循环体中加入Thread.sleep(1)、System.out.println()这类语句时,JIT不会对该循环做上述激进优化:

  • 像Thread.sleep()这类阻塞方法,线程从休眠状态恢复后,JVM会保证该线程重新读取主内存中的变量值,而非使用本地缓存;
  • 即使是普通语句,JIT也会因为循环体存在额外操作,放弃将变量缓存到寄存器的优化,线程会定期去主内存同步变量值,因此能看到其他线程修改后的lockSecond/lockThird,从而退出循环。这种情况虽然没有volatile保证严格的可见性,但在LeetCode的测试环境下,线程有足够的机会读取到最新值,因此能通过测试,只是轮询等待会消耗更多CPU资源,整体运行速度较慢。
  1. volatile的作用
    volatile关键字的核心作用是:
  • 禁止JIT对变量做缓存优化,强制线程每次读取变量都直接从主内存获取;
  • 禁止指令重排,保证变量修改的顺序性。
    因此方案3中使用volatile后,即使是空循环,线程也会持续读取主内存中的最新值,一旦其他线程修改变量,当前线程能立即感知,从而高效退出循环。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 17:36:15