深入理解volatile可见性原理与MESI缓存一致性协议

Posted 八阿哥克星

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解volatile可见性原理与MESI缓存一致性协议相关的知识,希望对你有一定的参考价值。

volatile作用——可见性

        大家都应该知道 volatile 的主要作用有两点: 1、保证变量的内存可见性 ;2、禁止指令重排序,今天我们重点来谈谈可见性。

        首先,在理解 volatile 可见性前,先来看一看一个多线程访问共享变量的例子。

public class VolatileExample 

    /**
     * main 方法作为一个主线程
     */
    public static void main(String[] args) 
        MyThread myThread = new MyThread();
        // 开启线程
        myThread.start();

        // 主线程执行
        for (; ; ) 
            if (myThread.isFlag()) 
                System.out.println("主线程访问到 flag 变量");
            
        
    



/**
 * 子线程类
 */
class MyThread extends Thread 

    private boolean flag = false;

    @Override
    public void run() 
        try 
            Thread.sleep(1000);
         catch (InterruptedException e) 
            e.printStackTrace();
        
        // 修改变量值
        flag = true;
        System.out.println("flag = " + flag);
    

    public boolean isFlag() 
        return flag;
    

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

        执行程序,我们会发现,控制台不会输出 “主线程访问到 flag 变量” 这句话。虽然子线程执行时已经将 flag 设置成 true,但主线程执行时没有读到 flag 的最新值,导致控制台没有输出上面的句子。这是因为JMM内存模型的不可见性;

JMM内存模型

        JMM(Java Memory Model):Java 内存模型,是 Java 虚拟机规范中所定义的一种内存模型,Java 内存模型是标准化的,屏蔽掉了底层不同计算机的区别。也就是说,JMM 是 JVM 中定义的一种并发编程的底层模型机制。

        JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。

JMM 的规定:所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。

  • 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
  • 线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
  • 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。

        然而,JMM 这样的规定可能会导致线程对共享变量的修改没有即时更新到主内存,或者线程没能够即时将共享变量的最新值同步到工作内存中,从而使得线程在使用共享变量的值时,该值并不是最新的。

        正因为 JMM 这样的机制,就出现了可见性问题。也就是我们上面那个例子出现的问题。那如何解决这个问题呢,这里就需要再引入总线的概念了。

        总线就是用来解决多个线程共享变量的副本数据的一致性问题;

        而我们今天的主角volatile也正是基于总线的基础上发挥作用的。

        我们都知道volatile可以保证多线程之间共享数据的可见性,实质上它的原理就是运用lock汇编指令触发总线嗅探机制,通俗的说呢,每个cpu在处理线程时会通过监听在总线上传播的数据来检查自己的缓存值(副本数据)是不是过期了,如果cpu发现自己缓存行对应的内存地址修改,就会将当前处理器的缓存行(副本数据)设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存中。这就是volatile保证可见性的原理了。

        那在上述过程中呢,共享数据会根据不同的情况处于不同的状态,这也就是今天的第二个重点,MESI缓存一致性协议;

MESI缓存一致性协议

        MESI分别代表缓存行数据所处的四种状态,通过对这四种状态的切换,来达到对缓存数据进行管理的目的。

状态 描述 监听任务
M 修改(Modify)该缓存行有效,数据被修改了,和内存中的数据不一致,数据只存在于本缓存行中缓存行必须时刻监听所有试图读该缓存行相对应的内存的操作,其他缓存须在本缓存行写回内存并将状态置为E之后才能操作该缓存行对应的内存数据
E 独享、互斥(Exclusive)该缓存行有效,数据和内存中的数据一致,数据只存在于本缓存行中缓存行必须监听其他缓存读主内存中该缓存行相对应的内存的操作,一旦有这种操作,该缓存行需要变成S状态
S 共享(Shared)该缓存行有效,数据和内存中的数据一致,数据同时存在于其他缓存中缓存行必须监听其他缓存是该缓存行无效或者独享该缓存行的请求,并将该缓存行置为I状态
I 无效(Invalid)该缓存行数据无效

MESI工作过程:

1、CPU1从内存中将变量a加载到缓存中,并将变量a的状态改为E(独享),并通过总线嗅探机制对内存中变量a的操作进行嗅探;

2、此时,CPU2读取变量a,总线嗅探机制会将CPU1中的变量a的状态置为S(共享),并将变量a加载到CPU2的缓存中,状态为S;

3、CPU1对变量a进行修改操作,此时CPU1中的变量a会被置为M(修改)状态,而CPU2中的变量a会被通知,改为I(无效)状态,此时CPU2中的变量a做的任何修改都不会被写回内存中(高并发情况下可能出现两个CPU同时修改变量a,并同时向总线发出将各自的缓存行更改为M状态的情况,此时总线会采用相应的裁决机制进行裁决,将其中一个置为M状态,另一个置为I状态,且I状态的缓存行修改无效) ;

4、CPU1将修改后的数据写回内存,并将变量a置为E(独占)状态;

5、此时,CPU2通过总线嗅探机制得知变量a已被修改,会重新去内存中加载变量a,同时CPU1和CPU2中的变量a都改为S状态;

在上述过程第3步中,CPU2的变量a被置为I(无效)状态后,只是保证变量a的修改不会被写回内存,但CPU2有可能会在CPU1将变量a置为E(独占)状态之前重新读取内存中的变量a,这个取决于汇编指令是否要求CPU2重新加载内存。

以上是关于深入理解volatile可见性原理与MESI缓存一致性协议的主要内容,如果未能解决你的问题,请参考以下文章

Java 并发编程线程共享变量可见性 ( volatile 关键字使用场景分析 | MESI 缓存一致性协议 | 总线嗅探机制 )

Juc13_JVM-JMM-CPU底层执行全过程缓存一致性协议MESI

volatile为啥不能保证原子性

深入理解Atomic原子操作和volatile非原子性

要是面试官再问我volatile,我就这么答

CPU中的MESI缓存最终一致性---CPU为什么需要缓存