方法内While循环卡住,字段自赋值可修复?信号量p()方法解析
这是个非常典型的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

