二、volatile关键字 - 内存可见性

本文深入解析Java中内存可见性的概念,探讨了多个线程间共享变量修改后的可见性问题,以及如何通过synchronized和volatile关键字解决。对比了volatile与synchronized的效率和适用场景,提供了代码示例。
1、内存可见性

​ (程序在运行时,jvm会为每一个执行任务的线程都分配一个独立的缓存,用于提高效率)

​ 我觉得可以这样来理解:

内存:啥是内存?就是可以理解成电脑当中的内存条,程序创建个变量,都放在内存当中(浅显理解)

可见性:就是多个线程在运行过程中,当某一个线程对共享的变量作出修改后,其他线程能不能看到该变量是否已经被改变的现象。

共享变量:线程在对该变量执行操作的时候,会从主内存中将该变量读到自己线程的缓存中去执行具体操作,执行完后再归还主内存,大家一起共享。

​ 理解了上面的三个概念后:

内存可见性:是指多个线程共享同一变量,如果某一线程对共享变量作出修改,可以被其他线程看到,那就说明该共享变量在各线程之间是可见的。

​ 因此,内存可见性问题,就是多个线程同时操作共享数据是,彼此产生了不可见的现象。

​ 解决这一问题,我们一般的思路就是使用同步锁(synchronized)来解决,因为同步锁能够保证内存的刷新,去主内存中读取数据,保证每次执行的都是最新的变量。但是,加锁这种情况会使程序的效率极低,因此,在解决内存可见性这一问题上,就出现了volatile关键字……..

2、volatile 关键字

​ Java 提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他线程,可以保证内存中的数据可见(原理:其调用了计算机底层的代码叫内存栅栏,时刻的将线程缓存中的数据刷新到主内存当中去,因此也就可以将volatile的操作理解为就是在主存中完成的)。

相较于synchronized关键字,可以将 volatile 看做一个轻量级的同步策略,但是又与锁有些不同:

  1⃣️    volatile不具备"互斥性",就是说多线程情况下,一个线程访问共享数据,其他多个线程也可以访问该数据,只不过是内存中完成而已

  2⃣️   volatile不能保证变量状态的“原子性操作”

效率:在解决内存可见性的问题上,volatile的效率要高于synchronized的效率,但是volatile的效率主要是浪费在指令重排序上。

代码示例:

public class TestVolatile {
    public static void main(String[] args){
        ThreadDemo td=new ThreadDemo();
        new Thread(td).start();
        while(true){
            if(td.isFlag()){
                System.out.println("-----------");
                break;
            }
        }
    }

}
class ThreadDemo implements Runnable{
    private volatile boolean flag=false;
    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag=true;
        System.out.println("flag="+isFlag());
    }
    public boolean isFlag(){
        return flag;
    }
    public void setFlag(boolean flag){
        this.flag=flag;
    }
}

结果:

-----------
flag=true

如果不使用volatile关键字

flag=true

转载于:https://www.cnblogs.com/zongheng14/p/10703820.html

<think>我们正在讨论volatile关键字如何通过内存屏障和缓存一致性实现可见性。 根据引用内容,我们可以总结如下: 1. 可见性的实现主要依靠两方面: a) 缓存一致性协议(如MESI) b) 内存屏障 2. 缓存一致性协议(如MESI)确保多个处理器核心之间缓存的一致性。 - 当一个线程修改了volatile变量,这个修改会立即被写回主内存(实际上是写回高速缓存,然后由缓存一致性协议保证其他缓存失效或更新)。 - 其他处理器通过总线嗅探机制(Bus Snooping)感知到该修改,并使自己缓存中对应的缓存行失效(Invalidate)。 - 这样,其他线程在读取该变量时,会发现缓存行已失效,从而从主内存(或通过缓存一致性协议从其他缓存)重新加载最新值。 3. 内存屏障则用于确保: a) 立即将写缓冲区的数据刷新到缓存(进而通过缓存一致性协议刷新到主内存) b) 立即将失效队列中的失效消息处理掉,保证后续读取时能拿到最新值。 具体来说: - 写屏障(Store Barrier):确保屏障之前的写操作(包括volatile写)的结果在屏障之后对其他处理器可见。具体来说,它会强制将写缓冲区(Store Buffer)中的数据刷新到高速缓存(或主内存),并等待失效确认(Invalidate Acknowledge)完成。 - 读屏障(Load Barrier):确保屏障之后的读操作(包括volatile读)能够获取到最新的值。具体来说,它会强制处理器在处理读操作之前,先处理完失效队列(Invalidate Queue)中的所有失效消息,从而使该处理器缓存中相应的缓存行失效,进而从主内存(或其他缓存)中重新加载。 4. 对于volatile变量,JVM会在其写操作之后插入一个写屏障,在读操作之前插入一个读屏障。 因此,volatile关键字可见性实现可以总结为以下步骤: 步骤1(写操作): 1.1 线程A修改volatile变量,写入自己的本地缓存(或写缓冲区)。 1.2 由于写屏障的作用,线程A必须等待该写操作被刷新到缓存(并最终到主内存)且收到所有其他处理器的失效确认(即该缓存行在其他缓存中的副本都被置为失效状态)后才能继续执行后续操作。 步骤2(读操作): 2.1 线程B读取volatile变量,由于读屏障的作用,它会先处理失效队列,将本地缓存中该变量的缓存行置为失效。 2.2 由于缓存行失效,线程B会从主内存(或通过缓存一致性协议从其他缓存)重新加载最新值。 这样,线程A的写操作结果对线程B就是可见的了。 下面我们用图示和代码说明: 图示: ```mermaid graph LR A[线程A:写volatile变量] --> B[插入写屏障] B --> C[强制刷新写缓冲区到缓存] C --> D[通过MESI协议使其他缓存行失效] D --> E[等待失效确认] E --> F[继续执行] G[线程B:读volatile变量] --> H[插入读屏障] H --> I[处理失效队列] I --> J[缓存行失效,重新从主内存加载] ``` 引用说明: - 内存屏障(写屏障和读屏障)的作用在引用[2]和引用[4]中有描述。 - 缓存一致性协议(MESI)在引用[1]和引用[4]中提及。 注意:这里的内存屏障是硬件层面的,JVM在编译时会在volatile写操作后插入写屏障指令,在volatile读操作前插入读屏障指令。 因此,volatile关键字可见性是通过内存屏障强制刷新缓存和使缓存失效,以及缓存一致性协议(如MESI)共同作用来实现的。 最后,我们生成相关问题。</think>### volatile 关键字如何通过内存屏障和缓存一致性实现可见性 #### 一、核心实现机制 volatile可见性通过 **硬件级缓存一致性协议** 和 **内存屏障** 协同实现,具体流程如下: 1. **缓存一致性协议(MESI)** - **总线嗅探(Bus Snooping)**: 当线程 A 修改 volatile 变量时,CPU 通过总线广播 `invalidate` 信号,通知其他核心的缓存失效[^4]。 $$ \text{volatile写} \rightarrow \text{总线广播} \rightarrow \text{其他缓存行失效} $$ - **失效队列处理**: 其他线程(如线程 B)在读取 volatile 变量前,必须处理失效队列中的 `invalidate` 消息,确保本地缓存副本无效[^4]。 ```mermaid graph LR A[线程A写volatile变量] --> B[强制刷回主内存] B --> C[总线广播invalidate信号] C --> D[线程B的缓存行失效] D --> E[线程B从主内存重新加载] ``` 2. **内存屏障(Memory Barrier)** - **写屏障(Store Barrier)**: 插入在 volatile 写操作之后,强制将写缓冲区(Store Buffer)数据刷回高速缓存/主内存[^4]。 ```java public void writeVolatile() { data = 42; // volatile写 // 隐式插入写屏障(StoreStore + StoreLoad) } ``` - **读屏障(Load Barrier)**: 插入在 volatile 读操作之前,强制处理失效队列并刷新本地缓存[^2]。 ```java public int readVolatile() { // 隐式插入读屏障(LoadLoad + LoadStore) return data; // volatile读 } ``` 3. **屏障组合实现可见性** | 操作序列 | 插入屏障类型 | 作用 | |------------------|----------------------------|----------------------------------------------------------------------| | volatile写 | `StoreStore` + `StoreLoad` | 确保写操作前的修改对所有线程可见,并防止与后续操作重排序[^1] | | volatile读 | `LoadLoad` + `LoadStore` | 确保后续操作能读到最新值,并防止与之前操作重排序[^2] | --- #### 、底层硬件协作流程 以 **x86 架构** 为例的完整可见性实现: 1. **写操作流程**: ```mermaid sequenceDiagram 线程A->>L1缓存: 修改volatile变量 L1缓存->>总线: 广播invalidate信号 总线->>其他CPU缓存: 使缓存行失效 L1缓存->>主内存: 通过Store屏障强制刷回数据 ``` - 关键指令:`lock addl $0x0,(%rsp)`(x86的StoreLoad屏障实现)[^1] 2. **读操作流程**: ```mermaid sequenceDiagram 线程B->>失效队列: 处理invalidate消息(Load屏障) 失效队列->>L1缓存: 标记缓存行为I(Invalid) 线程B->>主内存: 重新加载最新数据 ``` - 关键机制:**MESI协议状态转换**(Modified→Exclusive→Shared→Invalid)[^3] --- #### 三、可见性保障的本质 1. **强制主内存同步** - volatile 变量读写直接穿透 CPU 缓存层级,避免线程工作内存与主内存不一致[^1]。 $$ \text{volatile读写} \equiv \text{绕过工作内存直接访问主内存} $$ 2. **屏障的刷新语义** | 屏障类型 | 伪代码实现 | 作用 | |------------|-----------------------|-------------------------------| | 写屏障 | `flushStoreBuffer()` | 清空写缓冲区到缓存/内存[^4] | | 读屏障 | `processInvalidateQueue()` | 处理失效队列刷新缓存[^4] | 3. **与普通变量对比** ```java // 普通变量(无可见性保证) int a = 1; // 可能停留在写缓冲区或工作内存 // volatile变量(强制可见) volatile int b = 2; // 立即刷回主内存并通知其他缓存失效 ``` --- ### 总结:可见性实现三要素 1. **缓存失效机制**:通过 MESI 协议使其他核心缓存失效[^3] 2. **内存屏障强制刷新**: - 写屏障 → 数据刷回主内存 - 读屏障 → 加载最新数据[^2] 3. **禁止重排序**:确保操作顺序符合程序语义[^1] > ⚠️ **性能代价**:频繁 volatile 操作会触发大量缓存失效和屏障指令,在超线程环境下可能引发 **全局存储屏障(MFENCE)**,导致性能下降[^4]。 --- ### 相关问题 1. **MESI 协议中缓存行的四种状态如何转换?失效队列满时会发生什么?** 2. **x86 架构的 `LOCK` 指令前缀与 volatile 的 StoreLoad 屏障有何关系?** 3. **为什么 Java 不直接使用 `fence` 指令而用 `lock addl $0x0,(%rsp)` 实现屏障?** 4. **ZGC/Shenandoah 等新垃圾收集器如何优化 volatile内存屏障开销?** 5. **ARM 架构的 `DMB`/`DSB` 指令与 x86 内存屏障的映射关系是什么?**
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值