volatile内存模型

Java中的volatile

CPU的读写操作

CPU在cache line状态的转化期间是阻塞的,经过长时间的优化,在寄存器和L1缓存之间添加了LoadBuffer、StoreBuffer来降低阻塞时间,LoadBuffer、StoreBuffer,合称排序缓冲(Memoryordering Buffers (MOB)),Load缓冲64长度,store缓冲36长度,Buffer与L1进行数据传输时,CPU无须等待。

  • CPU执行load读数据时,把读请求放到LoadBuffer,这样就不用等待其它CPU响应,先进行下面操作,稍后再处理这个读请求的结果。
  • CPU执行store写数据时,把数据写到StoreBuffer中,待到某个适合的时间点,把StoreBuffer的数据刷到主存中。

内存屏障

内存屏障:内存屏障(memory barrier)是一个CPU指令。编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。

内存屏障分为以下四种:

  • LoadLoad屏障(Load1,LoadLoad, Load2):确保Load1所要读入的数据能够在被Load2和后续的load指令访问前读入。通常能执行预加载指令或/和支持乱序处理的处理器中需要显式声明Loadload屏障,因为在这些处理器中正在等待的加载指令能够绕过正在等待存储的指令。 而对于总是能保证处理顺序的处理器上,设置该屏障相当于无操作。
  • LoadStore屏障(Load1,LoadStore, Store2):确保Load1的数据在Store2和后续Store指令被刷新之前读取。在等待Store指令可以越过loads指令的乱序处理器上需要使用LoadStore屏障。
  • StoreStore屏障(Store1,StoreStore,Store2):确保Store1的数据在Store2以及后续Store指令操作相关数据之前对其它处理器可见(例如向主存刷新数据)。通常情况下,如果处理器不能保证从写缓冲或/和缓存向其它处理器和主存中按顺序刷新数据,那么它需要使用StoreStore屏障。
  • StoreLoad屏障(Store1,StoreLoad,Load2):确保Store1的数据在被Load2和后续的Load指令读取之前对其他处理器可见。StoreLoad屏障可以防止一个后续的load指令 不正确的使用了Store1的数据,而不是另一个处理器在相同内存位置写入一个新数据。

相应的HotSpot源码:

// Implementation of class OrderAccess.
inline void OrderAccess:loadload() { acquire(); }

inline void OrderAccess:storestore() { release(); }

inline void OrderAccess:loadstore() { acquire(); }

inline void OrderAccess:storeload() { fence(); }

inline void OrderAccess::acquire() {
    volatile intptr_t local_dummy;
    #ifdef AMD64
        __asm__ volatile ("movq 0(%%rsp), %0" : "=r" (local_dummy) : : "memory");
    #else
        __asm__ volatile ("movq 0(%%esp), %0" : "=r" (local_dummy) : : "memory");
    #endif //AMD64
}

inline void OrderAccess::release() {
    // 避免多线程触及同一高速缓存行
    volatile jint local_dummy = 0;
}

inline void OrderAccess::fence() {
    // 判断是否是多核,单核不进来
    if (os::is_MP()) {
        // always use locked addl since mfence is sometimes expensive
        #ifdef AMD64
            __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
        #else
            __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
        #endif
    }
}

Volatile的内存含义

  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。
  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
需要的屏障 第二个操作 第二个操作 第二个操作 第二个操作
第一个操作 普通读 普通写 volatile读/monitor enter volatile写/monitor exit
普通读 LoadStore
普通读 StoreStore
voaltile读/monitor enter LoadLoad LoadStore LoadLoad LoadStore
volatile写/monitor exit StoreLoad StoreStore

举个例子:

public class VolatileBarrierExample {
    int a;
    volatile int v1 = 1;
    volatile int v2 = 2;

    void readAndWrite() {
        int i = v1; // 第一个volatile读
        int j = v2; // 第二个volatile读
        a = i + j;  // 普通写
        v1 = i + 1; // 第一个volatile写
        v2 = j * 2; // 第二个volatile写
    }
}

处理器优化为:
volatile2

C++中的volatile

volatile

程序可能受到程序之外的因素影响。

C++中的volatile跟java的不一样,java的volatile在C++中类似的是atomic。

volatile可以避免优化、强制内存读取的顺序,但是并没有线程同步的语义,C++标准并不能保证它在多线程情况的正确性。

假设以下的代码:

class AObject
{
public:
    void wait()
    {
        m_flag = false;
        while (!m_flag)
        {
            this_thread::sleep(1000ms);
        }
    }
    void notify()
    {
        m_flag = true;
    }

private:
    volatile bool m_flag;
};

AObject obj;

...

// Thread 1
...
obj.wait();
...

// Thread 2
...
obj.notify();
...

Thread 2的修改导致缓存变脏,Thread 1读取内存会试图获取最新的数据,所以这段代码可以正常执行,但是并不代表volatile能够保证多线程的一致性,我们来看看然后看看标准C++基金https://isocpp.org 是怎么说的官方链接

ybh32aexkh

volatile:

  • 与平台无关的多线程程序,volatile几乎无用(Java和C#中的volatile除外);
  • volatile不保证原子性(一般需使用CPU提供的LOCK指令);
  • volatile不保证执行顺序;
  • volatile不提供内存屏障(Memory Barrier)和内存栅栏(Memory Fence);
  • 多核环境中内存的可见性和CPU执行顺序不能通过volatile来保障,而是依赖于CPU内存屏障。

注:volatile诞生于单CPU核心时代,为保持兼容,一直只是针对编译器的,对CPU无影响。

volatile在C/C++中的作用:

  • 告诉编译器不要将定义的变量优化掉;
  • 告诉编译器总是从缓存取被修饰的变量的值,而不是寄存器取值。

atomic

std::atomic<bool> 的操作是原子的,同时构建了良好的内存屏障。

C++实践中推荐涉及并发问题都使用std::atomic,只有涉及特殊内存操作的时候才使用volatile关键字。这些情况通常IO相关,防止相关操作被编译器优化,也是volatile关键字发明的本意。

C++ atomic的使用可能会遇到的情况:

std::atomic<bool> x;
x.store(true,std::memory_order_seq_cst);

通过atomic实现自旋锁:

std::atomic<bool> flag = ATOMIC_VAR_INIT(false);
//TODO 0 
bool expected = false;
while(!flag.compare_exchange_strong(expected, true, std::memory_order_acquire)) {
    expected = false;
    //TODO1
    flag.store(false, std::memory_order_release);
    //TODO2
}

这样子TODO0和TODO2的代码就不会在编译器的优化下越过加锁的位置跳到TODO1里面。

  • memory_order_acquire:执行该操作时,加入一个内存屏障,需要等待其他线程完成所有内存读
  • memory_order_release:执行该操作时,加入一个内存屏障,需要等待本线程完成所有内存写
  • memory_order_relaxed:完全不添加任何屏障
  • memory_order_consume:同acquire,但是该屏障并不阻塞无关的读操作,只阻塞有依赖关系的读写(不知道如何做到的,比较神奇)
  • memory_order_acq_rel:清空自己所在cpu的读写依赖
  • memory_order_seq_cst:最严格的屏障,要求所有cpu的读写严格依赖

reference:

https://benjaminwhx.com/2018/05/13/【细谈Java并发】内存模型之volatile/

https://liuzhengyang.github.io/2017/05/12/javamemorymodel/

http://kexianda.info/2017/04/28/从HotSpot源码看Java-volatile/

https://www.cnblogs.com/zhao-zongsheng/p/9092520.html

https://liam.page/2018/01/18/volatile-in-C-and-Cpp/

https://cloud.tencent.com/developer/article/1403223

https://mhy12345.xyz/technology/cplusplus-memory_order/

comments powered by Disqus