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

方法内While循环卡住,字段自赋值可修复?信号量p()方法解析

为什么Semaphore的while循环会死循环,自赋值却能解决?

这是个非常典型的Java内存可见性+JIT编译优化导致的问题,咱们一步步把它说清楚:

一、原while循环卡住的根本原因:内存可见性缺失+JIT优化

首先得回忆Java的内存模型规则:

  • 每个线程都有自己的「工作内存」,共享变量(比如你代码里的tokens信号量计数)存储在「主内存」中。
  • 线程读取共享变量时,会先把主内存里的值拷贝到自己的工作内存;修改后,再把工作内存的值写回主内存。
  • 如果没有同步机制(比如volatile、synchronized),JVM不保证线程之间的变量修改会及时同步。

回到你的场景:
你的p()方法大概是类似这样的:

public void p() {
    // 死循环等待令牌
    while (tokens <= 0) {
        // 空循环,什么都不做
    }
    tokens--;
}

而另一个线程调用的v()方法是:

public void v() {
    tokens++;
}

问题出在JIT编译器的循环不变量优化上:
JIT编译器看到你的while循环体里没有任何修改tokens的代码,会认为tokens的值在循环过程中不会变化,于是就把tokens的值缓存到CPU寄存器里——之后每次循环判断,都直接读寄存器里的旧值,根本不去主内存重新读取。

哪怕另一个线程调用v()把主内存里的tokens改成了大于0,当前线程的工作内存和寄存器里还是旧的0,所以循环会一直判断tokens <= 0,永远卡着。

二、为什么tokens = tokens;能解决问题?

这条看似毫无意义的自赋值语句,其实是打破了JIT的优化逻辑:
JIT编译器看到循环体里有对tokens的赋值操作(哪怕是把自己赋值给自己),就会认为「这个变量可能在循环过程中被外部修改」,因此不会再把它缓存到寄存器里。

接下来,每次循环判断tokens <= 0时,线程都会去主内存读取最新的tokens值——当另一个线程修改的tokens已经大于0时,当前线程就能读到这个新值,从而退出循环。

但正如你所说,这只是个“治标不治本”的偏方:它只解决了可见性问题,完全没解决原子性问题。比如多个线程同时调用p()或v()时,很容易出现竞态条件(比如两个线程同时读到tokens=1,都执行tokens--,最后tokens变成-1),所以线程安全性反而更差了。

三、正确的解决方式

要彻底解决这个问题,需要同时保证可见性和原子性:

  • 方式一:给tokens字段加上volatile修饰,保证内存可见性,同时禁止指令重排序。但注意volatile不保证原子性,所以tokens++和tokens--还是需要用synchronized或者原子类(比如AtomicInteger)来包裹。
  • 方式二:用synchronized修饰p()和v()方法,或者使用Lock接口,既保证可见性,又保证原子性,彻底解决线程安全问题。
  • 方式三:直接使用Java标准库的java.util.concurrent.Semaphore,它已经帮你处理好了所有线程安全细节。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.19 07:16:54