Volatile的内存可见性

本文深入探讨Java内存模型(JMM),解释线程间如何通过主存与工作内存交互,确保多处理器环境下共享变量的可见性。分析volatile关键字如何比synchronized更轻量级地解决可见性问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

内存模型
什么是JAVA 内存模型?

Java Memory Model (JAVA 内存模型)是描述线程之间如何通过内存(memory)来进行交互。 具体说来, JVM中存在一个主存区(Main Memory或Java Heap Memory),对于所有线程进行共享,而每个线程又有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作并非发生在主存区,而是发生在工作内存中,而线程之间是不能直接相互访问,变量在程序中的传递,是依赖主存来完成的。  简称JMM。每一个CPU中都有一个缓存,这个缓存一般存储线程中的变量,这样就影响了线程之间的可见性(降低了耦合)。

  但是很多的时候我们要确保多处理器开发中共享变量的可见性。从硬件层次上分析如何实现Volatile。

通过Lock前缀指令

1)将当前处理器缓存行的数据写回到系统内存。

2)这个写回内存操作会使在其他CPU中缓存了该内存地址的数据无效(通过嗅探在总线上传播的数据来检查自己缓存值是否过期)。

那么当写两条线程Thread-A与Threab-B同时操作主存中的一个volatile变量i时,Thread-A写了变量i,那么:

 

Thread-A发出LOCK#指令
发出的LOCK#指令锁总线(或锁缓存行),同时让Thread-B高速缓存中的缓存行内容失效
Thread-A向主存回写最新修改的i
Thread-B读取变量i,那么:

Thread-B发现对应地址的缓存行被锁了,等待锁的释放,缓存一致性协议会保证它读取到最新的值。

 

Java内存模型的抽象示意图如下: 

è¿éåå¾çæè¿°
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:

1、线程A把本地内存A中更新过的共享变量刷新到主内存中去。 
2、线程B到主内存中去读取线程A之前已更新过的共享变量。

说明白了内存模型,我们看一看什么是内存可见性?

内存可见性
内存可见性(Memory Visibility)是指当某个线程正在使用对象状态而另一个线程在同时修改该状态,需要确保当一个线程修改了对象状态后,其他线程能够立即看到发生的状态变化。

由于线程之间的交互都发生在主内存中,但对于变量的修改又发生在自己的工作内存中,经常会造成读写共享变量的错误,我们也叫可见性错误。

可见性错误是指当读操作与写操作在不同的线程中执行时,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。

解决方案
我们可以通过同步来保证对象被安全地发布。除此之外我们也可以使用一种更加轻量级的volatile变量,还可以使用ReentrantLock,CAS等等。

synchronized关键字
public class TestSynchronized {

    public static void main(String[] args) {

        ThreadDemo td = new ThreadDemo();
        new Thread(td).start();

        while (true) {
            synchronized (td) {
                if (td.getFlag()) {
                    System.out.println("主线程flag:" + td.getFlag());
                    break;
                }
            }
        }
    }
}

class ThreadDemo implements Runnable {

    //共享变量
    private boolean flag = false;

    public boolean getFlag() {
        return flag;
    }
    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {

        try {
            Thread.sleep(200);
        } catch (Exception e) {
        }

        flag = true;

        System.out.println("其他线程flag=" + getFlag());
    }
}

同步锁方案:会带来性能问题,效率特别低,造成线程阻塞。

volatile关键字
java 提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当多个线程进行操作共享数据时,可以保证内存中的数据可见。 相较于synchronized是一种较为轻量级的同步策略。

public class TestVolatile {

    public static void main(String[] args) {

        ThreadDemo td = new ThreadDemo();
        new Thread(td).start();

        while(true){
            if(td.getFlag()){
                System.out.println("主线程flag:" + td.getFlag());
                break;
            }
        }
    }
}

class ThreadDemo implements Runnable{
    //共享变量
    private volatile boolean  flag = false;

    public boolean getFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {

        try {
            Thread.sleep(200);
        } catch (Exception e) {
        }

        flag = true;

        System.out.println("其他线程flag=" + getFlag());
    }
}

volatile的读写操作的过程: 
(1)线程写volatile变量的过程: 
1、改变线程工作内存中volatile变量的副本的值 
2、将改变后的副本的值从工作内存刷新到主内存 
(2)线程读volatile变量的过程: 
1、从主内存中读取volatile变量的最新值到线程的工作内存中 
2、从工作内存中读取volatile变量的副本

volatile方案: 
1、能够保证volatile变量的可见性 
2、不能保证变量状态的”原子性操作(Atomic operations)”

虽然我们对Volatile的内存可见性有了一定理解,我们还需要对它有更加深刻的认识,不再多写了:面试时被问到了volatile ,找个文章总结一下(早点看到就好了)

 

注意的是:

读写volatile变量是原子的,包括64位的long和double。

锁总线和锁内存的区别是:

锁总线的话,所有处理器都无法执行读写内存的操作。消耗比较大。

锁缓存的话,就是无发对指定内存进行读写操作,对于其他地址内存而言都是所有处理器都是可以进行访问的。

原子操作:不可以中断的一个或者是一系列的操作。

 

延申可以查看:https://blog.youkuaiyun.com/HNUST_LIZEMING/article/details/88355963

<think>嗯,用户的问题是关于Java中volatile关键字如何保证内存可见性的,还希望有交互图来解释。我需要先理解内存可见性的基本概念,然后结合volatile的工作原理来回答。首先,用户可能已经知道JMM(Java内存模型)的基础,但需要更深入的机制解释。 用户提到了引用资料1到4,这些资料里提到JMM、volatile的作用、内存屏障和CPU执行流程。我需要整合这些信息。比如,引用4提到volatile通过某种机制确保修改后的值立即写入主内存,并让其他线程的缓存失效。这可能涉及到内存屏障和缓存一致性协议,比如MESI。 接下来,用户想要交互图示例。我需要考虑如何用图示来展示没有volatile时的可见性问题,以及使用volatile后如何解决。可能需要画两个场景:普通变量和volatile变量的对比,展示线程工作内存和主内存的交互。 然后,思考volatile的实现机制。引用3提到OrderAccess::storeload(),这应该是一个内存屏障,防止指令重排序。同时,引用2指出volatile遵循happens-before原则,禁止重排序影响多线程结果。需要解释编译器和CPU层面的重排序,以及内存屏障如何阻止它们。 用户可能存在的疑问点包括:为什么volatile不能保证原子性?可见性和原子性的区别是什么?这时候需要区分这两个概念,并指出volatile仅解决可见性,而原子性需要其他机制如synchronized。 最后,组织回答结构,先介绍可见性问题,再解释volatile的底层原理,结合交互图说明,最后总结注意事项。确保使用用户要求的格式,比如行内公式用$...$,正确的中文回答,并生成相关问题。</think>### Java volatile内存可见性原理及交互图解析 #### 一、内存可见性问题背景 在Java多线程环境下,普通变量修改的可见性可能失效。例如: - 线程A修改变量后,新值可能仅停留在**工作内存**(CPU缓存) - 线程B读取时仍从**主内存**获取旧值 $$ \begin{array}{ccc} \text{主内存} & \xrightarrow{\text{读取旧值}} & \text{线程B工作内存} \\ & \nwarrow & \downarrow \\ \text{线程A工作内存} & \xleftarrow{\text{写入新值}} & \\ \end{array} $$ #### 二、volatile实现原理 通过**JMM规范+硬件层支持**实现可见性保证: 1. **编译器层面** - 禁止对volatile变量的指令重排序 - 插入内存屏障(Memory Barrier) ```java // 写操作后插入StoreLoad屏障 OrderAccess::storeload(); ``` 2. **CPU层面** - 基于**缓存一致性协议**(如MESI) - 强制将工作内存修改立即刷新到主内存 - 使其他CPU核心的对应缓存行失效 #### 三、交互流程示意图 ```mermaid sequenceDiagram participant 主内存 participant 线程A participant 线程B 线程A->>主内存: 1. volatile写操作 Note over 线程A,主内存: 强制刷新到主内存内存-->>线程B: 2. 使缓存行失效 线程B->>主内存: 3. 重新读取最新值 ``` #### 四、实现机制详解 | 阶段 | 普通变量 | volatile变量 | |----------|----------------------|---------------------------| | 读取操作 | 可能从工作内存读取旧值 | 必须从主内存读取最新值 | | 写入操作 | 可能延迟写入主内存 | 立即刷新到主内存 | | 指令重排序 | 允许 | 禁止(通过内存屏障) | | 缓存一致性 | 依赖CPU协议被动更新 | 主动触发缓存失效通知 | #### 五、注意事项 1. **不保证原子性** - 例如`volatile int i=0; i++`仍需`synchronized`保证原子性 2. **适用场景** - 状态标志位(如`isRunning`) - 单次写入的多线程共享变量 3. **性能影响** - 频繁写操作时会影响性能(需频繁刷新内存) [^1]: JMM规范定义的内存访问规则 [^2]: volatile的happens-before特性 [^3]: 内存屏障的具体实现 [^4]: CPU缓存一致性协议支持
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值