玩碎JAVA之volatile与Memory Barriers

2022/2/8 20:12:46

本文主要是介绍玩碎JAVA之volatile与Memory Barriers,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

大师们的原文,确实比其他文章讲的透彻的很多!

JSR-133原文
JSR-133 FAQ
The JSR-133 Cookbook for Compiler Writers

  • volatile修饰的字段,适用于一个线程写,多个线程读的情况,不适用于多个线程写的情况,不然也会有安全性的问题。

volatile有2层语义:

  1. 可见性;
  2. 禁止指令重排;

以下段落来自JSR-133 FAQ
What does volatile do?
Volatile fields are special fields which are used for communicating state between threads. Each read of a volatile will see the last write to that volatile by any thread; in effect, they are designated by the programmer as fields for which it is never acceptable to see a “stale” value as a result of caching or reordering. The compiler and runtime are prohibited from allocating them in registers. They must also ensure that after they are written, they are flushed out of the cache to main memory, so they can immediately become visible to other threads. Similarly, before a volatile field is read, the cache must be invalidated so that the value in main memory, not the local processor cache, is the one seen. There are also additional restrictions on reordering accesses to volatile variables.

译文:Volatile字段用于不同线程间通信状态的字段。volatile的每次读取都会看到任何线程对该volatile的最后一次写入;在程序员看来就是那些因为缓存或重新排序而产生的陈旧的值。编译器和运行时被禁止在寄存器中分配它们。还必须确保volatile字段在写入之后,从缓存中刷到主存中,所以他们对其他线程是及时可见的。同样的,在volatile字段读取之前,必须使缓存失效,以便看到的是主存中的值,而不是本地处理器缓存中的值。对volatile字段的重新排序还有其他限制。

Under the old memory model, accesses to volatile variables could not be reordered with each other, but they could be reordered with nonvolatile variable accesses. This undermined the usefulness of volatile fields as a means of signaling conditions from one thread to another.
译文:在旧的内存模型下,对volatile字段的访问不能相互重新排序,但它们可以与非volatile字段的访问一起重新排序。这削弱了volatile字段作为从一个线程向另一个线程发送条件信号的有效性。

Under the new memory model, it is still true that volatile variables cannot be reordered with each other. The difference is that it is now no longer so easy to reorder normal field accesses around them. Writing to a volatile field has the same memory effect as a monitor release, and reading from a volatile field has the same memory effect as a monitor acquire. In effect, because the new memory model places stricter constraints on reordering of volatile field accesses with other field accesses, volatile or not, anything that was visible to thread A when it writes to volatile field f becomes visible to thread B when it reads f.
译文:在新的内存模型下,volatile变量之间仍然不能重新排序。不同的是,现在在volatile字段周围的普通字段重新排序也不再那么容易。volatile字段写入具有与释放监视器相同的内存效应,volatile字段读取具有与获取监视器相同的内存效应。事实上,因为新的内存模型对volatile字段访问与其他字段访问的重新排序有更严格的限制,因此,不管是否volatile字段,线程A在写入volatile字段f时可见的任何内容,在线程B读取f时都是可见的。

下面是volatile字段的例子:

class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }

  public void reader() {
    if (v == true) {
      //uses x - guaranteed to see 42.
    }
  }
}

Assume that one thread is calling writer, and another is calling reader. The write to v in writer releases the write to x to memory, and the read of v acquires that value from memory. Thus, if the reader sees the value true for v, it is also guaranteed to see the write to 42 that happened before it. This would not have been true under the old memory model. If v were not volatile, then the compiler could reorder the writes in writer, and reader’s read of x might see 0.
译文:假设一个线程调用writer,另一个线程调用reader。
在writer中,对v的写入发布了对x到内存中的写入,而对v的读取时,则也会从内存中获取最新的x的值。
因此,如果reader看到的v的值为true,那么也可以保证x已经被写入了42。
在旧的内存模型中这是不可能的。
如果v字段不是volatile的,writer线程中,编译器可能会重新排序v和x 2个字段的写入,reader线程读取的x的值可能为0。

Effectively, the semantics of volatile have been strengthened substantially, almost to the level of synchronization. Each read or write of a volatile field acts like “half” a synchronization, for purposes of visibility.
译文:实际上,volatile的语义得到了实质性的加强,几乎达到了同步的水平。为了可见性的目的,对volatile字段的每次读或写都相当于“半”同步。

Important Note: Note that it is important for both threads to access the same volatile variable in order to properly set up the happens-before relationship. It is not the case that everything visible to thread A when it writes volatile field f becomes visible to thread B after it reads volatile field g. The release and acquire have to “match” (i.e., be performed on the same volatile field) to have the right semantics.
译文:请注意,两个线程访问同一个volatile变量对于正确设置happens-before关系非常重要。并不是线程A在写入volatile字段f时看到的所有内容在线程B读取volatile字段g后就变得可见。release和acquire必须“匹配”(即,在同一个volatile字段上执行),以具有正确的语义。

例子一

public class Test {
    volatile int A;
    int B = 0;

    public static void main(String[] args) {
        Test test = new Test();
        test.test();
    }

    private void test() {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                B = 1;
                A = 1;
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("A:"+A);
                System.out.println("B:"+B);
            }
        });
        t2.start();
        t1.start();
    }
}
result 1:
A:0
B:0
result 2:
A:1
B:1
result 3:
A:0
B:1

!!!不会出现以下结果,因为volatile字段禁止了指令重排,B = 1;这句一定先于A = 1;这句执行!!!
A:1
B:0

如果我们只是把t1线程中的2个写操作的顺序换下,其他都不变,如下

Thread t1 = new Thread(new Runnable() {
	@Override
	public void run() {
		A = 1;
		B = 1;
	}
});

输出结果

result 1:
A:0
B:0
result 2:
A:1
B:1
result 3:说明发生了重排序,B=1的写操作排到了A=1的写操作前面
A:0
B:1
result 4: 这种结果,没出现过,但理论上,应该是可以出现的把???
A:1
B:0

重排序无法超越内存屏障!
the values that were visible to A before writing the volatile variable will be visible to B after reading the volatile variable:

参考:https://blog.csdn.net/pang5356/article/details/109226307
https://www.sohu.com/a/211287207_684445
https://baijiahao.baidu.com/s?id=1699081863576708156&wfr=spider&for=pc
https://www.baeldung.com/java-volatile

以下段落来自The JSR-133 Cookbook for Compiler Writers

Memory Barriers

LoadLoad Barriers
The sequence: Load1; LoadLoad; Load2
ensures that Load1’s data are loaded before data accessed by Load2 and all subsequent load instructions are loaded. In general, explicit LoadLoad barriers are needed on processors that perform speculative loads and/or out-of-order processing in which waiting load instructions can bypass waiting stores. On processors that guarantee to always preserve load ordering, the barriers amount to no-ops.
确保Load1的数据在加载Load2访问的数据和所有后续加载指令之前加载。通常,在执行投机负载和/或无序处理的处理器上需要显式的LoadLoad barrier,在无序处理中,等待的加载指令可以绕过等待的存储。在保证始终保持负载顺序的处理器上,障碍相当于零操作。

StoreStore Barriers
The sequence: Store1; StoreStore; Store2
ensures that Store1’s data are visible to other processors (i.e., flushed to memory) before the data associated with Store2 and all subsequent store instructions. In general, StoreStore barriers are needed on processors that do not otherwise guarantee strict ordering of flushes from write buffers and/or caches to other processors or main memory.
确保在与Store2和所有后续存储指令关联的数据之前,Store1的数据对其他处理器是可见的(即,刷新到内存中)。通常,对于不能保证从写缓冲区和/或缓存到其他处理器或主存的严格刷新顺序的处理器,需要StoreStore屏障。

LoadStore Barriers
The sequence: Load1; LoadStore; Store2
ensures that Load1’s data are loaded before all data associated with Store2 and subsequent store instructions are flushed. LoadStore barriers are needed only on those out-of-order procesors in which waiting store instructions can bypass loads.
确保Load1的数据在与Store2关联的所有数据和随后的存储指令被刷新之前被加载。LoadStore屏障只在那些无序的处理器上需要,在这些处理器中等待的存储指令可以绕过负载。

StoreLoad Barriers
The sequence: Store1; StoreLoad; Load2
ensures that Store1’s data are made visible to other processors (i.e., flushed to main memory) before data accessed by Load2 and all subsequent load instructions are loaded. StoreLoad barriers protect against a subsequent load incorrectly using Store1’s data value rather than that from a more recent store to the same location performed by a different processor. Because of this, on the processors discussed below, a StoreLoad is strictly necessary only for separating stores from subsequent loads of the same location(s) as were stored before the barrier. StoreLoad barriers are needed on nearly all recent multiprocessors, and are usually the most expensive kind. Part of the reason they are expensive is that they must disable mechanisms that ordinarily bypass cache to satisfy loads from write-buffers. This might be implemented by letting the buffer fully flush, among other possible stalls.
确保在Load2和所有后续加载指令访问数据之前,Store1的数据对其他处理器可见(即,刷新到主存)。StoreLoad屏障防止后续加载不正确地使用Store1的数据值而不是由一个不同的处理器执行从一个最近的存储到相同的位置。因此,在下面讨论的处理器上,StoreLoad只是严格地用于将存储与在barrier之前存储的相同位置的后续加载分开。StoreLoad障碍几乎在所有最新的多处理器上都需要,而且通常是最昂贵的一种。它们昂贵的部分原因是,它们必须禁用通常绕过缓存来满足写缓冲区负载的机制。这可以通过让缓冲区在其他可能的停顿中完全刷新来实现。

Inserting Barriers

  1. Issue a StoreStore barrier before each volatile store.
    (On ia64 you must instead fold this and most barriers into corresponding load or store instructions.)
    在每个volatile写之前发出一个StoreStore屏障
    在ia64上,你必须把这个和大多数屏障折叠成相应的加载或存储指令
  2. Issue a StoreStore barrier after all stores but before return from any constructor for any class with a final field.
    在返回带有final字段的任何类的构造函数之前所有的写操作后,发出一个StoreStore屏障
  3. Issue a StoreLoad barrier after each volatile store.
    Note that you could instead issue one before each volatile load, but this would be slower for typical programs using volatiles in which reads greatly outnumber writes. Alternatively, if available, you can implement volatile store as an atomic instruction (for example XCHG on x86) and omit the barrier. This may be more efficient if atomic instructions are cheaper than StoreLoad barriers.
    在每个volatile写之后发出一个StoreLoad屏障
    注意,您可以在每次volatile加载之前发出一个,但是对于使用volatile的典型程序来说,这样做会慢一些,因为在这些程序中,读操作的数量远远超过写操作。或者,如果可行,您可以将volatile存储实现为原子指令(例如x86上的XCHG),并省略barrier。如果原子指令比StoreLoad barrier便宜,这可能会更有效。
  4. Issue LoadLoad and LoadStore barriers after each volatile load.
    On processors that preserve data dependent ordering, you need not issue a barrier if the next access instruction is dependent on the value of the load. In particular, you do not need a barrier after a load of a volatile reference if the subsequent instruction is a null-check or load of a field of that reference.
    在每个volatile读之后发出一个LoadLoad和LoadStore屏障
    在保留依赖于数据顺序的处理器上,如果下一个访问指令依赖于加载的值,则不需要发出barrier。特别是,如果后续的指令是空检查或加载该引用的一个字段,则不需要在加载volatile引用之后设置barrier。
  5. Issue an ExitEnter barrier either before each MonitorEnter or after each MonitorExit.
    (As discussed above, ExitEnter is a no-op if either MonitorExit or MonitorEnter uses an atomic instruction that supplies the equivalent of a StoreLoad barrier. Similarly for others involving Enter and Exit in the remaining steps.)
    在每个MonitorEnter之前或每个MonitorExit之后发出一个ExitEnter障碍。
    (如上所述,如果MonitorExit或MonitorEnter使用了一个原子指令来提供与StoreLoad barrier等价的东西,那么ExitEnter就是一个空操作。其余步骤中涉及Enter和Exit的其他步骤也类似。)
  6. Issue EnterLoad and EnterStore barriers after each MonitorEnter.
    在每个MonitorEnter后发出EnterLoad和EnterStore障碍
  7. Issue StoreExit and LoadExit barriers before each MonitorExit.
    在每个MonitorExit前发出StoreExit和LoadExit屏障
  8. If on a processor that does not intrinsically provide ordering on indirect loads, issue a LoadLoad barrier before each load of a final field. (Some alternative strategies are discussed in this JMM list posting, and this description of linux data dependent barriers.)
    如果处理器在非直接负载上没有本质上提供排序,则在每次加载final字段之前发出LoadLoad barrier。


这篇关于玩碎JAVA之volatile与Memory Barriers的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程