(SUB)volatile详解与MESI

volatile是什么

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NpbmF0XzQyNDgzMzQx,size_1,color_FFFFFF,t_70

volatile是JVM中最轻量的同步机制。

作用:

  1. 64位写入的原子性。(不是传统的原子性:复合操作不具有原子性)
  2. 内存可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入,这个新值对于其他线程来说是立即可见的。
  3. 禁止重排序(实现有序性)。

如何保持可见性

 

不加volatile可能导致的问题

volitale不能保证原子性

不加volitale:

af26206382294a2d9eb1f329a69aae1f.png

加了volitale,不到10w

8ead1cde884248f0837a3a60e6f10205.png

虽然保证了count++之后的count值保证了可见性,但是count++过程没有保证原子性

要达到10w,对m方法加synchronized

问:是否需要加volatile

答:需要

new对象分三步:1.申请内存,赋值为默认值,2.初始化值,3.将指针指向申请的内存

指令重排可能会将2,3步换位置

如果不加volatile,当第一个线程sync初始化一半(1,3完成,INSTANCE不为null),此时第二个线程进入拿到初始化一半的对象,读取值错误

 单例模式

对变量值加了 volitile 之后,一个线程中的改变,在另一个线程中可以立刻看到。

饿汉式

/**
 * 饿汉式
 * 类加载到内存后,就实例化一个单例,JVM保证线程安全
 * 简单实用,推荐使用!
 * 唯一缺点:不管用到与否,类装载时就完成实例化
 * Class.forName("")
 * (话说你不用的,你装载它干啥)
 */
public class Mgr01 {
    private static final Mgr01 INSTANCE = new Mgr01();

    private Mgr01() {};

    public static Mgr01 getInstance() {
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        Mgr01 m1 = Mgr01.getInstance();
        Mgr01 m2 = Mgr01.getInstance();
        System.out.println(m1 == m2);
    }
}

懒汉式

/**
 * lazy loading
 * 也称懒汉式
 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题
 * 可以通过synchronized解决,但也带来效率下降
 */
public class Mgr06 {
    private static volatile Mgr06 INSTANCE; //JIT

    private Mgr06() {
    }

    public static Mgr06 getInstance() {
        if (INSTANCE == null) {
            //双重检查
            synchronized (Mgr06.class) {
                if(INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Mgr06();
                }
            }
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr06.getInstance().hashCode());
            }).start();
        }
    }
}

懒汉式双重检查为了防止并发,volitale为了防止重排序导致的拿到未初始化的对象

volatile复合操作

public class Main {
    public volatile int n;
    public static void main(String[] args) {
    }
    public void add() {
        n++;
    }
}
 
********************************************************
javap -c ../out/production/untitled/Main.class
********************************************************
 
Compiled from "Main.java"
public class Main {
  public volatile int n;
 
  public Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
 
  public static void main(java.lang.String[]);
    Code:
       0: return
 
  public void add();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field n:I
       5: iconst_1
       6: iadd
       7: putfield      #2                  // Field n:I
      10: return
}

实现原理

JMM层:内存屏障

深入理解java内存屏障(volatile实现原理)_内存屏障底层原理_java伟大航路的博客-CSDN博客

定义四种内存屏障是为了维护JMM内存模型,主要是以下三个准则

(1) 所有volatile读写之间相互序列化。volatile属性进行写操作后,其他CPU能马上读到最新值。

(2) volatile读取操作之后发生的非volatile读写不能乱序到其之前。非volatile读写发生在volatile读之前,可以乱序到其之后。

(3) volatile写操作之前发生的非volatile读写不能乱序到其之后。非volatile读写发生在volatile写之后,可以乱序到其之前。

第一点很好理解,就是volatile的可见性要求。

第二点是为了维护happens-before准则。保证前后操作基于最新的volatile读。给个具体场景进行理解:比如我要在进行volatile读后,根据读到的值进行一些代码逻辑操作,如果这些逻辑重排到了volatile读之前,则可以理解这些逻辑代码都是基于一个旧 volatile 值做的,即逻辑上不满足volatile是最新的值。

第三点和第二点类似,也是为了维护happens-before准则。保证volatile写后数据最新。比如我先进行一段代码逻辑,再进行volatile写,如果这些逻辑重排到volatile写之后,当其他cpu看到volatile写操作时,就无法确实volatile写操作之前的操作是否已经确实地发生了

JMM定义的内存屏障一共有4种:

屏障类型    指令说明
StoreStore    写写屏障,插入两个写之间,volatile写之前,禁止前面的普通写或volatile写与当前的volatile写发生重排序
StoreLoad     写读屏障,插入volatile写之后,禁止当前的volatile写与后面的volatile读发生重排序
LoadLoad    读读屏障,插入 volatile读之后,禁止当前的volatile读与后面普通读或volatile读发生重排序
LoadStore读写屏障,插入volatile读之后,禁止当前的volatile读与后面的普通写或volatile写发生重排序

 

由3个准则可推导

第一步\第二步普通读普通写volatile读volatile写
普通读   LoadStore
普通写   StoreStore
volatile读LoadLoadLoadStoreLoadLoadLoadStore
volatile写  StoreLoadStoreStore

可怕的jvm源码分析(不重要了解即可)

深入理解java内存屏障(volatile实现原理)_内存屏障底层原理_java伟大航路的博客-CSDN博客

x86处理器硬件层:lock前缀,MSEI协议

lock前缀指令的作用

1. 确保后续指令执行的原子性。在Pentium及之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很大。在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性,缓存锁定将大大降低lock前缀指令的执行开销。

2. LOCK前缀指令具有类似于内存屏障的功能,禁止该指令与前面和后面的读写指令重排序。

3. LOCK前缀指令会等待它之前所有的指令完成、并且所有缓冲的写操作写回内存(也就是将store buffer中的内容写入内存)之后才开始执行,并且根据缓存一致性协议,刷新store buffer的操作会导致其他cache中的副本失效。

MSEI协议

首先,volatile是java语言层面给出的保证,MSEI协议是多核cpu保证cache一致性(后面会细说这个一致性)的一种方法

对于汇编指令中执行加锁操作的变量,MESI协议在以下两种情况中也会失效:

            a. CPU不支持缓存一致性协议。

            b. 该变量超过一个缓存行的大小,缓存一致性协议是针对单个缓存行进行加锁,此时,缓存一致性协议无法再对该变量进行加锁,只能改用总线加锁的方式

缓存一致性协议(MESI)——缓存加锁协议-CSDN博客

MESI有四种状态:

0bcbea4e71ca4268a36e8d098f384232.png

 四种状态间的转换关系如下:

ad43b4ae7c4e407997f37737439f803c.jpg

 

M状态下,对应的转换如下:

(1)本地读:由于可以直接从本缓存行读取,所以状态仍然是M

(2)本地写:也是直接修改本地的缓存行,因此仍然是M

(3)远程读:由于缓存行的数据和主内存不同,为了让远程能够得到最新的数据,必需将缓存行的数据同步到主内存,然后远程从主内存中读取该数据。这是缓存行和主内存的数据保持了一致,而且数据在本地和远程cache中,因此状态编程S

(4)远程写:同上,缓存需要同步到主内存,然后由远程进行数据修改。这时,缓存行的数据不是远程修改后的数据,因此缓存需要编程无效I。

1493dc04504e4269961cf030efbdb87c.png

 E状态下的状态转换:

(1)本地读:保持不变

(2)本地写:写了之后,本地的缓存和主存的缓存不一致了,状态变成M

(3)远程读:这时有多个cache共享该数据,状态变成了S

(4)远程写:远程写了之后,本地的缓存和修改后的数据不一致了,状态变为I

b01d2606515d4f2cb3117b53b7a7c378.png

 S状态下的状态转换:

7a8c1674d57040f296ac76d3eab72c75.png

  I状态下的状态转换

093141852d9643afbc1df6f83d445fb9.png

————————————————
版权声明:本文为CSDN博主「don't_know」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/besthezhaowen/article/details/125575374

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值