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

Spring Controller中非volatile变量可见性与main方法差异原因

问题现象

以下为相关代码片段:

package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

class VolatileTest{
    private static boolean stop = false;
    public void go() throws Exception{

        new Thread(()->{
            System.out.println("thread started in " + Thread.currentThread().getName());
            int counter = 0;
            while (!stop){
                counter++;
            };

            // #1
            System.out.println("stopped at i=" + counter + " in " + Thread.currentThread().getName());

        }).start();

        Thread.sleep(1000);
        stop = true;
    }
}

@RestController
public class MyVolatileController {

    @GetMapping("/volatile/test")
    public String test(){
        new Thread(()->{
            try{
                new VolatileTest().go();
            }catch(Exception e){}
        }).start();

        return "volatile";
    }

    public static void main(String[] args) throws Exception{
        new Thread(()->{
            try{
                new VolatileTest().go();
            }catch(Exception e){}
        }).start();
    }

}

预期行为:调用测试后程序永远不会执行到#1位置,因为stop变量未被volatile修饰,其他线程对该变量的修改无法被当前线程即时感知。

实际运行存在明显差异:

  • 在类的public static main方法中启动该测试时,运行表现符合预期,程序无法走到#1位置
  • 在Spring Controller的接口方法中启动相同测试代码时,程序能够执行到#1位置
原因解释

首先要明确一个常见的认知偏差:非volatile修饰的变量,跨线程修改不可见不是必然发生的铁律,只是在缺少happens-before约束时,JIT编译器被允许做的一类优化——没有任何规则要求JVM必须让跨线程修改不可见。

两种场景表现不同,核心是JIT即时编译的优化触发条件不一致:

  • 直接运行main方法的时候,while (!stop) { counter++; }是非常纯粹的热循环,单线程下执行速度极快,JVM启动后很短时间内这段代码的执行次数就达到了C2编译的阈值。JIT分析字节码时发现循环内部根本没有修改stop变量的逻辑,就会做激进优化:直接把循环判断条件!stop替换成常量true,之后子线程根本不会再去读取主存里的stop值,哪怕主线程把stop改成true,子线程也完全感知不到,自然永远走不到#1的位置。
  • 运行在Spring接口里的时候情况完全不同:Spring Boot启动本身要加载大量类、执行海量初始化逻辑,JVM预热的代码根本不是你写的这个小循环。等你发请求触发这段逻辑时,首先这段代码是解释执行的,不会上来就做死循环优化;就算后续执行次数够了触发JIT,Spring运行时的容器逻辑(比如Tomcat线程调度、Spring的上下文管理、AOP拦截逻辑里都自带大量安全点检查、内存屏障)会限制JIT的优化程度,不会允许它把stop的判断直接优化成常量。子线程循环时会不定期从主存同步stop的值,等主线程把stop设为true之后,子线程迟早能读到这个修改,退出循环走到#1位置。

需要额外注意:Java内存模型从来没说过非volatile变量的跨线程修改一定不可见,它只保证有明确happens-before关系的时候修改一定可见,没有这个关系的时候,可见性是没有任何保障的——可能马上看到,可能很久之后看到,也可能永远看不到。main方法里跑出来的永远不退出,只是特定JDK版本、特定执行路径下刚好触发了优化的结果,不是什么固定的语言特性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 08:09:21