CPU缓存模型、内存模型、Volatile关键字作用以及Volatile关键字为什么不保证原子性简易图

首先拿一个例子,该例子简单明了的展示出一个问题:

如果flag 不加Volatile关键字时,每当另外一个线程对flag进行操作的时候,其他线程是感知不到的;当在flag 前加上Volatile关键字后,每当其他线程修改了flag值得时候,其他线程也都可以感知到flag进行了变更,会从读取最新的值。

这就是Volatile关键字的其中一个作用:保证可见性。

volatile只是保证了可见性,并不算是轻量级锁,其作用是: 1.保证可见性 2.禁止指令重排 3.不保证原子性

目录

一、VolatileDemo

 二、CPU多级缓存与可见性

 三、CPU缓存模型与Volatile关键字

四、java内存模型简易图(不加Volatile关键字类似上图)

 五、为了达到数据访问的一致的解决方案

六、Volatile关键字为什么不保证原子性


一、VolatileDemo

public class MyVolatileDemo {
    static int flag = 0;
    //加上了 volatile关键字 每次修改flag值 第一个线程立即感知到flag发生改变
//    static volatile int flag = 0;

    public static void main(String[] args) {
        new Thread(() -> {
            int localFlag = flag;
            while (true) {
                //如果 flag 不加volatile关键字 这个线程一直感知不到flag发生改变,一直读取的是旧值,所以一直不会执行
                if (localFlag != flag) {
                    System.out.println("我感知到flag值发生了变化 " + flag);
                    localFlag = flag;
                }
            }
        }).start();

        new Thread(() -> {
            int localFlag = flag;
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("标志位被修改为:" + ++localFlag + "...");
                //flag值 每秒钟都在被修改
                flag = localFlag;
            }
        }).start();
    }
}

不加Volatile关键字时,运行结果:

加上关键字,运行结果:

static volatile int flag = 0

 二、CPU多级缓存与可见性

提到可见性的问题,我们可以简单分析一下为什么会造成其他线程不能实时感知到flag的更改。这根计算机cpu多级缓存有关;cpu内部有多级缓存,在操作数据是在自己内部的高速缓存中进行,这样大大提高了cpu处理效率,到一定时候会刷入主存。如果多个cpu直接读取主存中的数据,会造成效率和性能的低下。画个简单的图解:

 三、CPU缓存模型与Volatile关键字

         不加Volatile关键字——多线程并发问题

四、java内存模型简易图(不加Volatile关键字类似上图)

比如两个线程同时读取flag=0,其中一个线程1读取之后一直进行操作,并且会刷回主内存。这时线程2每次循环读取自己工作内存中初始加载的值,并未感知其他线程修改了flag原来的值,所以在并发执行时造成数据的不一致 。

flag 加上volatile关键字,当线程1对flag进行操作后,flag=1。assign步骤将flag=1写入到共工作内存,并且也会被强制刷入到主内存中;此时主存中flag=0 ——> flag = 1;此时线程2中,立马感知到flag内修改,自己本地工作内存中flag=0失效,并重新读取主内存的flag值,即更新后的flag=1。

 五、为了达到数据访问的一致的解决方案

总线加锁:

总线锁就是将cpu和内存之间的通信锁住,使得在锁定期间,其他cpu处理器不能操作其他内存中数据,故总线锁开销比较大。

总线锁的实现是采用cpu提供的LOCK 信号,当一个cpu在总线上输出此信号时,其他cpu的请求将被阻塞,那么该cpu则独占共享内存

缓存一致性协议:为了达到数据访问的一致,需要各个处理器在访问 高速缓存 时遵循一些协议,在读写时根据协议来操作,常见的协议有MSI、MESI、MOSI。最常见的是MESI协议。

在MESI协议中,每个高速缓存的 缓存控制器 不仅知道自己的读写操作,而且也监听其他告诉缓存的读写操作。共有四种状态,分别是:

  • M(Modify)表示共享数据只缓存在当前CPU缓存中,并且是被修改的状态。此时表示当前CPU缓存数据与主内存中不一致,其他CPU缓存中如果缓存了当前数据应是无效状态,因为该数据已被修改且并没更新到主内存
  • E(Exclusive)表示缓存的独占状态,数据只缓存在当前CPU缓存中,并且没有被修改
  • S(Shared)表示数据可能被多个CPU缓存,并且各个缓存中的数据和主内存中的数据一致
  • I(Invalid)表示当前缓存已经失效

当Flag变量 加上Volatile关键字后,作用就跟缓存一致性协议类似,但一个线程修改数据之后,会强制刷入主存,其他线程也在监听自己内存的flag是否被修改,如果被修改,则当前flag过期,立即去主存加载最新flag值。以达到数据保持一致。

 

六、Volatile关键字为什么不保证原子性

我们都知道Volatile关键字的三个特点,这里不做介绍。但Volatile关键字并不能保证原子性,知其然也要知其所以然,今天增加一个简易图来简单阐述Volatile关键字不能保证原子性的原因。

 上面图中有五个步骤,线程2在自己工作内存操作flag++时,只是基于之前内存中的值,即使工作内存感知到flag被线程1修改之后,但线程2操作仍然是之前读取的flag=0,也不能影响线程在flag=0的基础上进行++操作,最终线程2assign flag=1重新赋值给工作内存中的flag=1。最后写入主存仍然是与线程1操作结果一样,仍然是flag=1。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Nikola TesIa

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值