Java基础快速入门:volatile与synchronized解决线程可见性问题

本文纲要

  1. 问题引入:从结婚基金说起
  2. 多线程可见性问题代码复现
  3. Java内存模型图解(为什么会出现问题)
  4. 使用volatile关键字解决
  5. 使用synchronized同步块解决
  6. volatilesynchronized 解决可见性的对比
  7. 总结

问题引入:从结婚基金说起

假设小皮和小路共同攒了一笔结婚基金,金额为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;} 男孩线程栈
女孩线程栈
堆内存 - 唯一

  1. 读取
  2. 读取
  3. 修改并写回
  4. 女孩仍读副本

未同步主存,看到仍是10万
money = 100000
变量副本

money = 100000
变量副本

money = 100000 → 90000

过程解释:

  1. 两个线程各自从主存拷贝一份 money = 100000 到自己的线程栈;
  2. 男孩线程修改副本为 90000,刷新回主存;
  3. 女孩线程并不知道主存已更新,仍然读取自己的副本 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 ) 执行流程

  1. 男孩线程获取锁 → 修改money为90000 → 释放锁,同时将值刷新到主存;
  2. 女孩线程获取锁 → 清空本地副本 → 从主存读取最新值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。

总结

  1. 多线程环境下,一个线程对共享变量的修改可能对其他线程不可见,称为可见性问题
  2. 根本原因在于线程在使用变量时倾向于使用自己的本地副本,而非直接操作主存。
  3. volatile 关键字可以强制线程每次读取变量时都从主存获取最新值,适合"一写多读"场景。
  4. synchronized 同步块在进入锁时会清空本地副本并从主存加载,释放锁时刷新回主存,从而也能保证可见性,并且提供原子性保障。
  5. 选择方案时需权衡性能与功能:仅需可见性用 volatile;需要原子性或复合操作保护时用synchronized

理解好Java内存模型中的主存与线程本地副本关系,是掌握多线程编程的重要基础。