本文纲要
- 问题引入:从结婚基金说起
- 多线程可见性问题代码复现
- Java内存模型图解(为什么会出现问题)
- 使用
volatile关键字解决 - 使用
synchronized同步块解决 volatile与synchronized解决可见性的对比- 总结
问题引入:从结婚基金说起
假设小皮和小路共同攒了一笔结婚基金,金额为10万元。每个人都把这个数字记在心里(即直接使用已知的值),而不是每次去银行查询。
某天,小皮取出了1万元充值游戏,银行余额变为9万,但小路并不知道,心里依然以为是10万。
这就导致一个严重问题:数据已经发生变化,但另一方仍旧使用旧值。
把上述场景映射到多线程:
- 男孩线程(小皮):修改共享数据
- 女孩线程(小路):读取共享数据
- 共享数据(结婚基金余额):多线程共享的变量
当男孩线程修改了共享变量后,女孩线程如果没有及时感知到最新值,就会一直使用过时的副本数据,导致逻辑错误。
多线程可见性问题代码复现
1 ) 项目结构
dir
myvolatile-problem/ (无volatile,存在可见性问题)
└── src/com/wb/myvolatile/
├── Demo.java // 测试入口
├── Money.java // 共享数据
├── MyThread1.java // 女孩线程
└── MyThread2.java // 男孩线程
2 ) 核心代码
共享变量 Money.java (第一版:未使用volatile)
java
public class Money {
public static int money = 100000; // 无volatile
}
女孩线程 MyThread1.java
java
public class MyThread1 extends Thread {
@Override
public void run() {
while(Money.money == 100000){
// 如果余额还是10万,就不停循环
}
System.out.println("结婚基金已经不是十万了");
}
}
男孩线程 MyThread2.java (睡眠后修改余额)
java
public class MyThread2 extends Thread {
@Override
public void run() {
try {
Thread.sleep(10); // 模拟延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
Money.money = 90000; // 修改共享变量
}
}
测试类 Demo.java
java
public class Demo {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
t1.setName("小路同学");
t1.start();
MyThread2 t2 = new MyThread2();
t2.setName("小皮同学");
t2.start();
}
}
运行结果
程序没有输出,女孩线程陷入死循环,永远无法看到money已经被改为90000。
这就是典型的多线程可见性问题:一个线程对共享变量的修改,对其他线程不可见。
Java内存模型图解(为什么会出现问题)
在JVM内存模型中:
- 堆内存:唯一,存放所有对象和静态变量。
- 栈内存(线程栈):每个线程独有,存放局部变量和运行时状态。
当线程需要访问堆中的共享变量时,为了提高性能,线程会先复制一份变量副本到自己的线程栈(变量副本)中。后续对该变量的读写首先操作这个副本,必要时才与主内存(堆)同步。
#mermaid-svg-Z8tH1KuUOgd7cfFP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Z8tH1KuUOgd7cfFP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Z8tH1KuUOgd7cfFP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Z8tH1KuUOgd7cfFP .error-icon{fill:#552222;}#mermaid-svg-Z8tH1KuUOgd7cfFP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Z8tH1KuUOgd7cfFP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Z8tH1KuUOgd7cfFP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Z8tH1KuUOgd7cfFP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Z8tH1KuUOgd7cfFP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Z8tH1KuUOgd7cfFP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Z8tH1KuUOgd7cfFP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Z8tH1KuUOgd7cfFP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Z8tH1KuUOgd7cfFP .marker.cross{stroke:#333333;}#mermaid-svg-Z8tH1KuUOgd7cfFP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Z8tH1KuUOgd7cfFP p{margin:0;}#mermaid-svg-Z8tH1KuUOgd7cfFP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Z8tH1KuUOgd7cfFP .cluster-label text{fill:#333;}#mermaid-svg-Z8tH1KuUOgd7cfFP .cluster-label span{color:#333;}#mermaid-svg-Z8tH1KuUOgd7cfFP .cluster-label span p{background-color:transparent;}#mermaid-svg-Z8tH1KuUOgd7cfFP .label text,#mermaid-svg-Z8tH1KuUOgd7cfFP span{fill:#333;color:#333;}#mermaid-svg-Z8tH1KuUOgd7cfFP .node rect,#mermaid-svg-Z8tH1KuUOgd7cfFP .node circle,#mermaid-svg-Z8tH1KuUOgd7cfFP .node ellipse,#mermaid-svg-Z8tH1KuUOgd7cfFP .node polygon,#mermaid-svg-Z8tH1KuUOgd7cfFP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Z8tH1KuUOgd7cfFP .rough-node .label text,#mermaid-svg-Z8tH1KuUOgd7cfFP .node .label text,#mermaid-svg-Z8tH1KuUOgd7cfFP .image-shape .label,#mermaid-svg-Z8tH1KuUOgd7cfFP .icon-shape .label{text-anchor:middle;}#mermaid-svg-Z8tH1KuUOgd7cfFP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Z8tH1KuUOgd7cfFP .rough-node .label,#mermaid-svg-Z8tH1KuUOgd7cfFP .node .label,#mermaid-svg-Z8tH1KuUOgd7cfFP .image-shape .label,#mermaid-svg-Z8tH1KuUOgd7cfFP .icon-shape .label{text-align:center;}#mermaid-svg-Z8tH1KuUOgd7cfFP .node.clickable{cursor:pointer;}#mermaid-svg-Z8tH1KuUOgd7cfFP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Z8tH1KuUOgd7cfFP .arrowheadPath{fill:#333333;}#mermaid-svg-Z8tH1KuUOgd7cfFP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Z8tH1KuUOgd7cfFP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Z8tH1KuUOgd7cfFP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Z8tH1KuUOgd7cfFP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Z8tH1KuUOgd7cfFP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Z8tH1KuUOgd7cfFP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Z8tH1KuUOgd7cfFP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Z8tH1KuUOgd7cfFP .cluster text{fill:#333;}#mermaid-svg-Z8tH1KuUOgd7cfFP .cluster span{color:#333;}#mermaid-svg-Z8tH1KuUOgd7cfFP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Z8tH1KuUOgd7cfFP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Z8tH1KuUOgd7cfFP rect.text{fill:none;stroke-width:0;}#mermaid-svg-Z8tH1KuUOgd7cfFP .icon-shape,#mermaid-svg-Z8tH1KuUOgd7cfFP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Z8tH1KuUOgd7cfFP .icon-shape p,#mermaid-svg-Z8tH1KuUOgd7cfFP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Z8tH1KuUOgd7cfFP .icon-shape .label rect,#mermaid-svg-Z8tH1KuUOgd7cfFP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Z8tH1KuUOgd7cfFP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Z8tH1KuUOgd7cfFP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Z8tH1KuUOgd7cfFP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 男孩线程栈
女孩线程栈
堆内存 - 唯一
- 读取
- 读取
- 修改并写回
- 女孩仍读副本
未同步主存,看到仍是10万
money = 100000
变量副本
money = 100000
变量副本
money = 100000 → 90000
过程解释:
- 两个线程各自从主存拷贝一份
money = 100000到自己的线程栈; - 男孩线程修改副本为
90000,刷新回主存; - 女孩线程并不知道主存已更新,仍然读取自己的副本
100000,因此陷入死循环。
结论:如果不加控制,线程优先使用自己的变量副本,会导致"脏读"。
使用volatile关键字解决
volatile 强制线程每次读取变量时都直接访问主存中的最新值,而不使用线程本地副本。这样可以保证变量的可见性。
1 ) 修改 Money.java
仅在变量声明前添加 volatile:
java
public class Money {
public static volatile int money = 100000;
}
其他代码不变。再次运行后,女孩线程能立刻检测到money != 100000,
输出:
log
结婚基金已经不是十万了
2 ) volatile 的语义
- 保证可见性:一个线程修改变量后,新值立即刷新到主存,其他线程读取主存获取最新值。
- 不保证原子性(需单独使用synchronized或Lock等)。
- 禁止指令重排序。
3 ) 项目结构
dir
myvolatile/ (添加volatile解决)
└── src/com/wb/myvolatile/
├── Demo.java
├── Money.java // 增加了volatile
├── MyThread1.java
└── MyThread2.java
使用synchronized同步块解决
synchronized 也能够强制线程从主存中重新读取变量,因为进入同步块时会清空线程本地副本并重新从主存获取,退出时也会将修改刷新回主存。
1 ) 修改方案
Money.java (添加锁对象)
java
public class Money {
public static Object lock = new Object();
public static int money = 100000; // 未加volatile,最终靠同步块保证可见性
}
女孩线程 MyThread1.java (在同步块内判断)
java
public class MyThread1 extends Thread {
@Override
public void run() {
while(true){
synchronized (Money.lock){
if(Money.money != 100000){
System.out.println("结婚基金已经不是十万了");
break;
}
}
}
}
}
男孩线程 MyThread2.java (修改操作也在同步块内)
java
public class MyThread2 extends Thread {
@Override
public void run() {
synchronized (Money.lock) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
Money.money = 90000;
}
}
}
测试类 Demo.java 不变。
2 ) 执行流程
- 男孩线程获取锁 → 修改
money为90000 → 释放锁,同时将值刷新到主存; - 女孩线程获取锁 → 清空本地副本 → 从主存读取最新值
90000→ 发现不等于100000 → 打印并退出循环。
3 ) 同步块保证可见性的步骤
#mermaid-svg-8N4QJ9hIp02MRtoV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8N4QJ9hIp02MRtoV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8N4QJ9hIp02MRtoV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8N4QJ9hIp02MRtoV .error-icon{fill:#552222;}#mermaid-svg-8N4QJ9hIp02MRtoV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8N4QJ9hIp02MRtoV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8N4QJ9hIp02MRtoV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8N4QJ9hIp02MRtoV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8N4QJ9hIp02MRtoV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8N4QJ9hIp02MRtoV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8N4QJ9hIp02MRtoV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8N4QJ9hIp02MRtoV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8N4QJ9hIp02MRtoV .marker.cross{stroke:#333333;}#mermaid-svg-8N4QJ9hIp02MRtoV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8N4QJ9hIp02MRtoV p{margin:0;}#mermaid-svg-8N4QJ9hIp02MRtoV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8N4QJ9hIp02MRtoV .cluster-label text{fill:#333;}#mermaid-svg-8N4QJ9hIp02MRtoV .cluster-label span{color:#333;}#mermaid-svg-8N4QJ9hIp02MRtoV .cluster-label span p{background-color:transparent;}#mermaid-svg-8N4QJ9hIp02MRtoV .label text,#mermaid-svg-8N4QJ9hIp02MRtoV span{fill:#333;color:#333;}#mermaid-svg-8N4QJ9hIp02MRtoV .node rect,#mermaid-svg-8N4QJ9hIp02MRtoV .node circle,#mermaid-svg-8N4QJ9hIp02MRtoV .node ellipse,#mermaid-svg-8N4QJ9hIp02MRtoV .node polygon,#mermaid-svg-8N4QJ9hIp02MRtoV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8N4QJ9hIp02MRtoV .rough-node .label text,#mermaid-svg-8N4QJ9hIp02MRtoV .node .label text,#mermaid-svg-8N4QJ9hIp02MRtoV .image-shape .label,#mermaid-svg-8N4QJ9hIp02MRtoV .icon-shape .label{text-anchor:middle;}#mermaid-svg-8N4QJ9hIp02MRtoV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8N4QJ9hIp02MRtoV .rough-node .label,#mermaid-svg-8N4QJ9hIp02MRtoV .node .label,#mermaid-svg-8N4QJ9hIp02MRtoV .image-shape .label,#mermaid-svg-8N4QJ9hIp02MRtoV .icon-shape .label{text-align:center;}#mermaid-svg-8N4QJ9hIp02MRtoV .node.clickable{cursor:pointer;}#mermaid-svg-8N4QJ9hIp02MRtoV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8N4QJ9hIp02MRtoV .arrowheadPath{fill:#333333;}#mermaid-svg-8N4QJ9hIp02MRtoV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8N4QJ9hIp02MRtoV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8N4QJ9hIp02MRtoV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8N4QJ9hIp02MRtoV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8N4QJ9hIp02MRtoV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8N4QJ9hIp02MRtoV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8N4QJ9hIp02MRtoV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8N4QJ9hIp02MRtoV .cluster text{fill:#333;}#mermaid-svg-8N4QJ9hIp02MRtoV .cluster span{color:#333;}#mermaid-svg-8N4QJ9hIp02MRtoV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-8N4QJ9hIp02MRtoV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8N4QJ9hIp02MRtoV rect.text{fill:none;stroke-width:0;}#mermaid-svg-8N4QJ9hIp02MRtoV .icon-shape,#mermaid-svg-8N4QJ9hIp02MRtoV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8N4QJ9hIp02MRtoV .icon-shape p,#mermaid-svg-8N4QJ9hIp02MRtoV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8N4QJ9hIp02MRtoV .icon-shape .label rect,#mermaid-svg-8N4QJ9hIp02MRtoV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8N4QJ9hIp02MRtoV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8N4QJ9hIp02MRtoV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8N4QJ9hIp02MRtoV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
线程请求锁
获得锁
清空线程本地变量副本
从主内存重新读取共享变量到副本
执行同步代码块
修改了共享变量?
将副本刷新到主内存
直接结束
释放锁
4 ) 项目结构
dir
myvolatile2/ (synchronized方案)
└── src/com/wb/myvolatile2/
├── Demo.java
├── Money.java // 含lock对象,未使用volatile(或可同时存在)
├── MyThread1.java // 同步块判断
└── MyThread2.java // 同步块修改
volatile与synchronized解决可见性的对比
| 特性 | volatile | synchronized |
|---|---|---|
| 可见性 | 保证,每次读写直接操作主存 | 保证,进入/退出同步块时与主存同步 |
| 原子性 | 不保证(仅单次读/写为原子操作,但复合操作如++不安全) | 保证,锁内的代码块具有原子性 |
| 性能开销 | 低,不涉及线程阻塞和上下文切换 | 较高,涉及锁竞争、线程阻塞/唤醒 |
| 适用场景 | 状态标记、一写多读、简单的可见性需求 | 复合操作、需要原子性保障的临界区 |
| 释放锁/清空副本 | 不使用锁,直接读主存 | 获取锁后会清空线程本地变量副本 |
在本案例中,两种方式都能解决问题:
volatile适合简单状态标记,如本例中只有一个线程修改,另一个线程检查;synchronized功能更强,但开销更大。如果仅需保证可见性且无复合操作,推荐使用volatile。
总结
- 多线程环境下,一个线程对共享变量的修改可能对其他线程不可见,称为可见性问题。
- 根本原因在于线程在使用变量时倾向于使用自己的本地副本,而非直接操作主存。
volatile关键字可以强制线程每次读取变量时都从主存获取最新值,适合"一写多读"场景。synchronized同步块在进入锁时会清空本地副本并从主存加载,释放锁时刷新回主存,从而也能保证可见性,并且提供原子性保障。- 选择方案时需权衡性能与功能:仅需可见性用
volatile;需要原子性或复合操作保护时用synchronized。
理解好Java内存模型中的主存与线程本地副本关系,是掌握多线程编程的重要基础。