并发编程-volatile

volatile

前言

复习volatile实现原理之前先复习下JMM

CPU缓存和主内存的关系模型:

CPU为了解决其运算速度和内存读写速度不匹配的矛盾,CPU运算速度比内存读写快超级多

解决方法:高速缓存

cpu附近有L1,L2,L3三级缓存,之后就是主内存,引入了三级缓存带来的就是缓存的一致性问题,需要用到缓存一致性协议去解决缓存一致性问题,如MESI

缓存行

在总结MESI缓存一致性协议前先看看缓存行的定义:

  • 缓存是分段的,一个段代表一个存储空间,即缓存行,也是CPU缓存中可分配的最小单位。

MESI

在MESI缓存一致性协议中定义了四种状态:

  • M,modified(已修改),缓存跟主存数据不一致,如果别的CPU要读主存这一块数据,该缓存需要先回写到主存且状态变为S(Share可共享)
  • E,exclusive(独占),缓存和主存数据一致,别的CPU要读这块数据时,状态变为S,要修改这块数据时,状态变为M, 在其他缓存中的数据副本被标记为I(无效)
  • S,share(可共享),其他CPU也有这个数据,且数据是一致的
  • I,invalid(无效),当其他CPU修改了自己也有的缓存数据时,会通知自己将这块数据置为无效,下次自己要读时必须从主存读取

MESI是基于嗅探机制让其他处理器的缓存行失效的:

  • 只有缓存行处于独占E或者已修改M才能去进行缓存行的写操作,也只有在这两种状态下,缓存行是独占的,即这个缓存行只有自己的这份拷贝。
  • 所以,当处理器想要写缓存,必须先获取到缓存行的独占权,且必须先发送一条指令给总线(我要开始写缓存),其他处理器因为一直在嗅探总线,所以会立马感知到这条指令,从而让自身处理器对应的缓存行失效(如果有)

JMM

JMM,java内存模型其实和CPU内存模型非常像,在JMM中,每个线程都有自己的工作内存,工作内存中的数据是主存数据的拷贝,线程和线程之间的数据通信必须依赖于主存,不能直接访问其他线程的工作内存,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。

所以,通过JMM也可以看出,如果没有做同步处理,java多线程环境下也是会出现数据一致性问题,线程和线程之间的工作内存中的数据可能不一致

而volatile为多线程下的可见性问题提供了保证

一,内存语义&原理

  • 通过内存屏障,禁止指令重排序,保证了有序性
  • 保证了共享变量的可见性
  • 不保证原子性

内存屏障:

内存屏障分为:

  • 读屏障
    • 让缓存中的数据失效,强制从主存中读取
  • 写屏障
    • 将缓存中的新值强制写回到主存,让其他线程可见

作用:

  • 禁止屏障两边的指令重排序
  • 强制将写缓冲区/CPU高速缓存(L1,L2)中的数据写回到主内存,或者让相应缓存中的数据失效

编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。编译器选择了一个比较保守的JMM内存屏障插入策略,这样可以保证在任何处理器平台,任何程序中都能得到正确的volatile内存语义。这个策略是:(Load代表读操作,Store代表写操作)

  • 在每个volatile写操作前插入一个StoreStore屏障;
  • 在每个volatile写操作后插入一个StoreLoad屏障;
  • 在每个volatile读操作后插入一个LoadLoad屏障;
  • 在每个volatile读操作后再插入一个LoadStore屏障。

volatile与普通变量的重排序规则:

  • 如果第一个操作是volatile读,那无论第二个操作是什么,都不能重排序;
  • 如果第二个操作是volatile写,那无论第一个操作是什么,都不能重排序;
  • 如果第一个操作是volatile写,第二个操作是volatile读,那不能重排序。

原理:

java代码经过处理得到汇编代码后:

0x0000000002931351: lock add dword ptr [rsp],0h  ;*putstatic instance

发现多了一个lock前缀

lock前缀作用:

  • 早期锁总线,其他CPU对缓存行的读写操作会被阻塞,直到锁释放,后期锁缓存行,且lock操作后的写操作结果会回写到主内存,并且使其他CPU该缓存行失效。其他CPU在用到该缓存行时发现缓存行失效,会从主存中读取最新值

二,用途

  • 单例模式
public class Singleton {

    private static Singleton instance; // 不使用volatile关键字

    // 双重锁检验
    public static Singleton getInstance() {
        if (instance == null) { // 第7行
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // 第10行
                }
            }
        }
        return instance;
    }
}

主要是对象实例化的new指令不是一个原子指令,包含了内存分配,调用init,引用指向对象地址这几个过程,cpu为了程序效率可能将其重排序了,返回出去的对象引用可能没有init初始化

  • 共享状态变量

猜你喜欢

转载自blog.csdn.net/weixin_41922289/article/details/107437806
今日推荐