“后续读取”在volatile变量的上下文中意味着什么?

问题描述 投票:13回答:5

Java memory visibility documentation说:

在每次后续读取同一字段之前,会发生对易失性字段的写入。

我很困惑多线程背景下的后续意义。这句话是否意味着所有处理器和内核都有一些全局时钟。那么例如我在某个线程的循环c1中为变量赋值,然后第二个线程能够在后续循环c1 + 1中看到这个值?

java multithreading cpu-architecture
5个回答
6
投票

听起来像它说它在线程之间提供无锁的获取/释放内存排序语义。请参阅Jeff Preshing's article explaining the concept(主要用于C ++,但文章的主要观点是语言中立和关于概念。)

事实上,Java volatile提供了顺序一致性,而不仅仅是acq / rel。但是,没有实际的锁定。请参阅Jeff Preshing的文章,了解为什么命名符合您使用锁定所做的操作。)


如果读者看到你写的值,那么它就知道在写之前生产者线程中的所有内容都已经发生了。

此排序保证仅在与单个线程内的排序保证相结合时才有用。

EG

int data[100];
volatile bool data_ready = false;

制片人:

data[0..99] = stuff;
 // release store keeps previous ops above this line
data_ready = true;

消费者:

while(!data_ready){}     // spin until we see the write
// acquire-load keeps later ops below this line
int tmp = data[99];      // gets the value from the producer

如果data_ready不是易失性的,那么读它就不会在两个线程之间建立一个先发生过的关系。

你不必有一个spinloop,你可以从volatile int读取序列号或数组索引,然后读取data[i]


我不太了解Java。我认为volatile实际上为您提供顺序一致性,而不仅仅是发布/获取。顺序释放存储不允许在以后加载时重新排序,因此在典型的硬件上,它需要昂贵的内存屏障,以确保在允许任何后续加载执行之前刷新本地核心的存储缓冲区。

Volatile Vs Atomic解释了更多关于volatile给你的订购。

Java volatile只是一个排序关键字;它不等同于C11 _AtomicC++11 std::atomic<T>,它们也为您提供原子RMW操作。在Java中,volatile_var++不是原子增量,它是一个单独的加载和存储,如volatile_var = volatile_var + 1。在Java中,你需要像AtomicInteger这样的类来获得原子RMW。

请注意,C / C ++ volatile并不意味着原子性或排序;它只告诉编译器假设该值可以异步修改。除了最简单的情况之外,这只是您无需编写任何内容所需的一小部分内容。


3
投票

我很困惑多线程背景下的后续意义。这句话是否暗示了所有处理器和内核的全局时钟......?

后来的手段(根据字典)及时到来。当然,计算机中的所有CPU都有一个全局时钟(想想X Ghz),文档试图说如果线程1在时钟节拍1处做了某事,那么线程2在时钟节拍2处在另一个CPU上执行某些操作,它是行动被认为是后续的。

在每次后续读取同一字段之前,会发生对易失性字段的写入。

可以添加到这句话以使其更清楚的关键短语是“在另一个线程中”。理解它可能更有意义:

对volatile字段的写入发生在每个后续读取另一个线程中的相同字段之前。

这是说如果在Thread-1中的(及时)写入之后在线程-2中读取了volatile字段,则将保证线程-2看到更新的值。在documentation you point to中更进一步的部分(强调我的):

...只有在读取操作之前发生写入操作时,一个线程的写入结果才能保证对另一个线程的读取可见。 synchronized和volatile构造以及Thread.start()和Thread.join()方法可以形成先发生关系。特别是。

注意突出显示的短语。只要重新排序不违反语言的定义,Java编译器就可以自由地重新排序任何一个线程执行中的指令,只要重新排序不违反语言的定义 - 这称为执行顺序,并且与程序顺序截然不同。

让我们看看以下示例,其中变量ab是非易失性整数,初始化为0而没有synchronized子句。显示的是程序顺序和线程遇到代码行的时间。

Time     Thread-1        Thread-2
1        a = 1;          
2        b = 2;          
3                        x = a;
4                        y = b;
5        c = a + b;      z = x + y;

如果Thread-1在时间5添加a + b,则保证为3。但是,如果Thread-2在时间5添加x + y,它可能会得到0,1,2或3取决于竞争条件。为什么?因为效率原因,编译器可能已经重新排序了Thread-1中的指令以在a之后设置b。此外,Thread-1可能没有适当地发布ab的值,因此Thread-2可能会过时。即使Thread-1被上下文切换或跨越写入内存屏障并且发布了ab,Thread-2也需要越过读屏障来更新ab的任何缓存值。

如果ab被标记为volatile那么写a必须发生 - 之前(就可见性保证而言)随后在第3行读取a并且写入b必须发生 - 在随后读取第4行的b之前。两个线程都会获得3。

我们在java中使用volatilesynchronized关键字来确保事先发生的保证。当分配volatile或退出synchronized块时,交叉写入存储器屏障,当读取volatile或进入synchronized块时,读取屏障被越过。 Java编译器无法通过这些内存屏障重新排序写入指令,因此可确保更新顺序。这些关键字控制指令重新排序并确保正确的内存同步。

注意:在单线程应用程序中不需要volatile,因为程序顺序可确保读取和写入保持一致。单线程应用程序可能会在第3和第4时看到(非易失性)ab的任何值,但由于语言保证,它在时间5总是看到3。因此,尽管使用volatile会更改单线程应用程序中的重新排序行为,但只有在线程之间共享数据时才需要它。


2
投票

这意味着一旦某个线程写入易失性字段,所有其他线程将观察(在下次读取时)该写入值;但这并不能保护你免受比赛。

线程有自己的缓存,这些缓存将通过缓存一致性协议无效并使用新写入的值进行更新。

编辑

后续意味着只要在写入之后发生这种情况。由于你不知道发生这种情况的确切周期/时间,你通常会说当其他一些线程观察到写入时,它会观察在写入之前完成的所有操作;因此,波动性建立了先发生的保证。

有点像一个例子:

 // Actions done in Thread A
 int a = 2;
 volatile int b = 3;


 // Actions done in Thread B
 if(b == 3) { // observer the volatile write
    // Thread B is guaranteed to see a = 2 here
 }

您也可以循环(旋转等待),直到您看到3为例。


1
投票

这更多地定义了不会发生什么,而不会发生什么。

基本上它是说,一旦发生了对atomic变量的写入,就不会有任何其他线程在读取变量时将读取过时的值。

考虑以下情况。

  • 线程A不断递增atomica
  • 线程B偶尔读取A.a并将该值暴露为非原子b变量。
  • 线程C偶尔读取A.aB.b

鉴于aatomic,有可能推断出从C的角度来看,b可能偶尔会小于a,但绝不会大于a

如果a不是原子的,则不能给予这样的保证。在某些缓存情况下,C很可能在任何时候都能看到b超越a的进展。

这是一个简单的演示,说明Java内存模型如何允许您推断在多线程环境中可以发生什么和不可以发生什么。在现实生活中,读取和写入数据结构之间的潜在竞争条件可能要复杂得多,但推理过程是相同的。


1
投票

Peter's answer给出了Java内存模型设计背后的基本原理。 在这个答案中,我试图仅使用JLS中定义的概念进行解释。


在Java中,每个线程都由一组动作组成。 其中一些操作有可能被其他线程观察(例如,编写共享变量),这些操作称为同步操作。

将线程的动作写入源代码的顺序称为程序顺序。 订单定义之前和之后(或更好,而不是之前)。

在一个线程中,每个动作都有一个before-before关系(用<表示)和下一个(在程序顺序中)动作。这种关系很重要,但很难理解,因为它非常基础:它保证如果A <B则A的“效果”对B是可见的。 这确实是我们在编写函数代码时所期望的。

考虑

Thread 1           Thread 2

  A0                 A'0
  A1                 A'1
  A2                 A'2
  A3                 A'3

然后通过程序顺序我们知道A0 <A1 <A2 <A3并且A'0 <A'1 <A'2 <A'3。 我们不知道如何订购所有行动。 它可以是A0 <A'0 <A'1 <A'2 <A1 <A2 <A3 <A'3或具有素数交换的序列。 但是,每个这样的序列必须具有根据线程的程序顺序对每个线程的单个动作进行排序。

两个程序订单不足以订购每个操作,它们是部分订单,与我们正在寻找的总订单相反。

根据发生的可测量时间(如时钟)将操作连续放置的总顺序称为执行顺序。 这是动作实际发生的顺序(仅请求动作看起来按此顺序发生,但这只是一个优化细节)。

到目前为止,动作不是在线程间(在两个不同的线程之间)排序的。 同步操作用于此目的。 每个同步动作至少与另一个同步动作同步(它们通常成对出现,如写入和读取易失性变量,锁定和互斥锁的解锁)。

同步关系是线程之间发生的事情(前者暗示后者),它作为一个不同的概念暴露,因为1)稍微发生 - 之前发生 - 硬件自然强制执行同时可能需要软件干预。

happen-before从程序顺序派生,与同步顺序同步(用<<表示)。 同步顺序是根据两个属性定义的:1)它是一个总顺序2)它与每个线程的程序顺序一致。

让我们为我们的线程添加一些同步动作:

Thread 1           Thread 2

  A0                 A'0
  S1                 A'1
  A1                 S'1
  A2                 S'2
  S2                 A'3

程序订单很简单。 什么是同步顺序?

我们正在寻找1)包括所有S1,S2,S'1和S'2以及2)必须具有S1 <S2和S'1 <S'2的东西。

可能的结果:

S1 < S2 < S'1 < S'2
S1 < S'1 < S'2 < S2
S'1 < S1 < S'2 < S'2

所有都是同步命令,没有一个同步命令但很多,上面的问题是错误的,它应该是“什么是同步命令?”。

如果S1和S'1使得S1 << S'1比我们将可能的结果限制在S1 <S'2那样,那么上面的结果S'1 <S1 <S'2 <S'2是现在被禁止了。

如果S2 << S'1那么唯一可能的结果是S1 <S2 <S'1 <S'2,当只有一个结果时我相信我们有连续的一致性(相反的情况并非如此)。

注意,如果A << B这些并不意味着代码中存在强制执行命令的机制,其中A <B。 同步操作受同步顺序影响,它们不会对其进行任何实现。 一些同步动作(例如,锁)强加特定的执行顺序(从而强加同步顺序),但是一些不执行(例如,挥发物的读/写)。 它是创建同步顺序的执行顺序,这与synchronize-with关系完全正交。


简而言之,“后续”形容词指的是任何同步顺序,即包含所有同步动作的任何有效(根据每个线程程序顺序)顺序。


然后,JLS继续定义何时发生数据竞争(当两个冲突的访问未按发生前的顺序排序时)以及它在发生前的意义 - 在一致之前。 那些超出了范围。

© www.soinside.com 2019 - 2024. All rights reserved.