摘要
可见性问题是多线程并发中最隐蔽的 Bug 之一:线程对共享变量的修改无法被其他线程及时感知。本文通过真实案例展示问题成因,剖析底层机制,并给出解决方案,帮助你避免"明明赋值了却没生效"的陷阱。
一、什么是可见性问题?
在多线程环境下,每个线程会从主内存中读取共享变量的副本存放在 工作内存(寄存器、CPU 缓存)。线程对变量的修改可能:
- 只作用在自己的工作内存中;
- 没有立即刷新到主内存;
- 导致其他线程无法感知。
这就是 可见性问题。
二、真实案例 1:线程无法停止
问题代码
java
class Worker extends Thread {
private boolean running = true;
@Override
public void run() {
System.out.println("线程启动");
while (running) {
// 模拟业务逻辑
}
System.out.println("线程结束");
}
public void stopRunning() {
running = false;
}
}
public class VisibilityDemo {
public static void main(String[] args) throws InterruptedException {
Worker worker = new Worker();
worker.start();
Thread.sleep(1000);
worker.stopRunning();
System.out.println("stopRunning 已调用");
}
}
现象
- 主线程调用
stopRunning()
后,子线程依旧 死循环不退出。 - 部分机器上能复现,部分机器上却正常运行。
原因
running
没有声明为volatile
。- 子线程一直从自己的工作内存读取
running=true
,无法感知主线程的修改。
解决办法
java
private volatile boolean running = true;
三、真实案例 2:配置热更新失效
问题场景
在某个 Web 应用中,配置项(如开关、策略)存放在一个对象里:
java
class Config {
public boolean enableFeature = false;
}
业务代码:
java
while (true) {
if (config.enableFeature) {
doSomething();
}
}
管理员修改配置:
ini
config.enableFeature = true;
现象
即使修改了配置,业务线程仍然执行旧逻辑,无法生效。
原因
- 配置对象没有加同步措施,线程读到的还是旧值。
解决办法
- 使用
volatile
:
java
class Config {
public volatile boolean enableFeature = false;
}
- 使用 原子引用:
java
AtomicReference<Config> configRef = new AtomicReference<>(new Config());
四、真实案例 3:双重检查锁单例失效
问题代码
java
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
现象
在高并发下,可能得到"未初始化完成"的对象,导致 NullPointerException。
原因
-
JVM 可能对
new Singleton()
重排:- 分配内存
- 将引用赋值给 instance
- 执行构造函数
-
导致另一个线程读到 instance 不为 null,但对象还没初始化。
解决办法
arduino
private static volatile Singleton instance;
volatile 禁止了指令重排,保证可见性。
五、可见性问题的本质
- CPU 缓存:多核 CPU 下,每个核心有独立缓存,数据可能不同步。
- 编译器优化:可能重排指令,改变执行顺序。
- 缺少内存屏障:没有同步手段约束顺序和刷新,线程只能看到旧值。
六、解决可见性问题的工具
-
volatile
- 保证变量的可见性和有序性。
- 适合状态标志、开关。
-
synchronized
- 保证可见性、原子性和有序性。
- 适合复合操作。
-
Lock(ReentrantLock)
- 类似 synchronized,更灵活。
-
原子类(AtomicXXX)
- 内部通过 CAS 和 volatile 保证可见性与原子性。
七、实践建议
- 场景简单(状态标志、配置开关) → 使用
volatile
。 - 需要原子性(计数器、复合逻辑) → 使用
AtomicXXX
或synchronized
。 - 高并发复杂逻辑 → 使用并发包中的
Lock
或Concurrent
工具类。
八、总结
可见性问题往往表现为:
- 明明修改了变量,线程却看不到;
- 在单线程正常运行,多线程下出问题;
- 不同机器、不同 JDK 版本结果不一致。
它的根源是 JMM 的工作内存与主内存隔离 。
解决方法是:使用 volatile、synchronized 或并发工具类,让修改对所有线程立刻可见。
一句话总结:可见性问题是最常见的并发 Bug,volatile 是最轻量的解决方案,但要结合具体场景合理使用。