"subsequent read" 在 volatile 变量的上下文中意味着什么?
What does "subsequent read" mean in the context of volatile variables?
Java memory visibility documentation 表示:
A write to a volatile field happens-before every subsequent read of that same field.
我对后续在多线程上下文中的含义感到困惑。这句话是否暗示了所有处理器和内核的一些全局时钟。因此,例如,我在某个线程的循环 c1 中为变量赋值,然后第二个线程能够在后续循环 c1 + 1 中看到该值?
这意味着一旦某个线程写入到一个volatile字段,所有其他线程将观察(在下一次读取时)写入的值;但这并不能保护你免受种族歧视。
线程有自己的缓存,这些缓存将失效并通过缓存一致性协议更新为新写入的值。
编辑
后续意味着每当发生这种情况时在 写入本身之后。由于您不知道确切的 cycle/timing 何时会发生,您通常会说当某个其他线程观察到写入时,它将观察到该写入之前完成的所有操作;因此 volatile 建立了 happens-before 保证。
有点像示例:
// 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。
这更像是对不会发生什么而不是会发生什么的定义。
本质上是说,一旦 write 到 atomic
变量发生,就不会有任何其他线程在读取变量时读取陈旧的值。
考虑以下情况。
线程 A 正在连续递增 atomic
值 a
。
线程 B 偶尔读取 A.a
并将该值公开为
非原子 b
变量。
线程C偶尔会同时读取A.a
和B.b
。
鉴于 a
是 atomic
可以推断,从 C 的角度来看,b
可能偶尔是小于 a
但 绝不会大于 a
.
如果 a
是 不是 原子,则无法提供此类保证。在某些缓存情况下,C 很可能随时看到 b
超过 a
的进度。
这是一个简单的演示,说明 Java 内存模型如何让您推理 在多线程环境中什么可以发生,什么不能发生。在现实生活中,读取和写入数据结构之间的潜在竞争条件可能要复杂得多,但推理过程是相同的。
在我看来,它在线程 之间提供无锁acquire/release 内存排序语义。请参阅 Jeff Preshing's article explaining the concept(主要针对 C++,但文章的要点是语言中立的并且是关于概念的。)
事实上Javavolatile
提供顺序一致性,而不仅仅是acq/rel。但是,没有实际的锁定。请参阅 Jeff Preshing 的文章,了解为什么命名与您对锁所做的操作相匹配。)
如果 a reader看到你写的值,那么它就知道生产者线程中的一切在写之前也已经发生了。
此排序保证仅与有关在单个线程内排序的其他保证结合使用。
例如
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
不是易变的,读取它不会在两个线程之间建立 happens-before 关系。
您不必有自旋循环,您可以从 volatile int
读取序列号或数组索引,然后读取 data[i]
.
我不太了解Java。我认为 volatile
实际上为您提供了顺序一致性,而不仅仅是 release/acquire。顺序释放存储不允许在以后的加载中重新排序,因此在典型的硬件上,它需要一个昂贵的内存屏障来确保在允许执行任何以后的加载之前刷新本地核心的存储缓冲区。
Volatile Vs Atomic 详细解释 volatile
给你的顺序。
Java volatile
只是一个排序关键字;它 not 等同于 C11 _Atomic
或 C++11 std::atomic<T>
,它们也为您提供原子 RMW 操作。在 Java 中,volatile_var++
是 而不是 原子增量,它是一个单独的加载和存储,如 volatile_var = volatile_var + 1
。在 Java 中,你需要像 AtomicInteger
这样的 class 来获得原子 RMW。
请注意 C/C++ volatile
根本不暗示原子性或顺序;它只是告诉编译器假设该值可以异步修改。除了最简单的情况,这只是你需要为任何东西编写无锁代码的一小部分。
给出了 Java 内存模型设计背后的基本原理。
在这个答案中,我试图仅使用 JLS 中定义的概念进行解释。
在 Java 中,每个线程都由一组 操作组成。
其中一些动作有可能被其他线程观察到(例如写一个共享变量),这些
称为 同步操作 。
线程的动作在源代码中的编写顺序称为程序顺序。
一个顺序定义什么是 before 和什么是 after(或者更好的是,not before)。
在一个线程中,每个动作与 next(按程序顺序)具有 happens-before 关系(用 < 表示)行动。
这种关系很重要,但很难理解,因为它非常基础:它保证如果 A < B 那么
A 的 "effects" 对 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 或交换质数的序列。
但是,每个这样的序列都必须让每个线程的单个操作根据线程的程序顺序进行排序。
这两个程序命令不足以命令每个动作,它们是部分命令,与
总订单 我们正在寻找。
根据可测量的时间(如时钟)将操作排成一行的总顺序称为执行顺序。
它是动作实际发生的顺序(只要求动作出现发生在
这个顺序,但这只是一个优化细节)。
到目前为止,这些操作没有在线程间(两个不同线程之间)排序。
同步操作就是为了这个目的。
每个同步动作 同步 至少另一个同步动作(它们通常成对出现,比如
volatile 变量的写入和读取,互斥量的锁定和解锁)。
synchronize-with关系是thread之间的happens-before(前者暗示后者),暴露为
一个不同的概念,因为 1) 它略微是 2) happens-before 在同步时由硬件自然强制执行
可能需要软件干预。
happens-before 源自程序顺序,synchronize-with 来自同步顺序(用<< 表示)。
同步顺序根据两个属性定义: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
全部都是同步命令,没有一个同步命令而是很多,上面的问题是错误的,它
应该是"What are the synchronization orders?"。
如果 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 的顺序。
同步操作受同步顺序影响,它们不会强加任何具体化。
一些同步操作(例如锁)强加了特定的执行顺序(因此是同步顺序)但有些则没有(例如 reads/writes of volatiles)。
创建同步顺序的是执行顺序,这与同步关系完全正交。
长话短说,"subsequent"形容词指的是任意同步顺序,即任意有效(根据每个线程
程序顺序)包含所有同步操作的顺序。
然后 JLS 继续定义数据竞争何时发生(当两个冲突的访问未按 happens-before 排序时)
以及发生之前一致的含义。
这些超出了范围。
I'm confused what does subsequent means in context of multithreading. Does this sentence implies some global clock for all processors and cores...?
随后的意思是(根据字典)及时之后。计算机中的所有 CPUs 肯定有一个全局时钟(想想 X Ghz)并且文档试图说如果线程 1 在时钟节拍 1 做某事然后线程 2 在另一个 CPU 在时钟滴答 2 时,它的动作被认为是后续的。
A write to a volatile field happens-before every subsequent read of that same field.
可以添加到这句话中以使其更清楚的关键短语是 "in another thread"。理解为:
可能更有意义
A write to a volatile field happens-before every subsequent read of that same field in another thread.
这是什么意思 如果 在线程 1 中写入之后(及时)在线程 2 中读取 volatile
字段,则线程-2 将保证看到更新后的值。在 documentation you point to 的更上方是部分(强调我的):
... The results of a write by one thread are guaranteed to be visible to a read by another thread only if the write operation happens-before the read operation. The synchronized and volatile constructs, as well as the Thread.start() and Thread.join() methods, can form happens-before relationships. In particular.
注意突出显示的短语。 Java 编译器可以出于优化目的自由地在任何一个线程的执行中重新排序指令,只要重新排序不违反语言的定义——这称为 执行 顺序并且与程序顺序截然不同。
让我们看下面的示例,其中变量 a
和 b
是初始化为 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
。但是,如果线程 2 在时间 5 添加 x + y
,它可能会得到 0、1、2 或 3,具体取决于竞争条件。为什么?因为出于效率原因,编译器可能已将 Thread-1 中的指令重新排序以在 b
之后设置 a
。此外,Thread-1 可能没有适当发布 a
和 b
的值,因此 Thread-2 可能会得到过时的值。即使 Thread-1 被上下文切换或越过写入内存屏障并且 a
和 b
已 已发布,Thread-2 也需要越过读取屏障更新 a
和 b
.
的任何缓存值
如果 a
和 b
被标记为 volatile
那么对 a
的写入必须发生在(就可见性保证而言)后续读取 a
在第 3 行和对 b
的写入必须发生在后续读取 b
在第 4 行之前。两个线程都将获得 3.
我们在 java 中使用 volatile
和 synchronized
关键字来确保 happens-before 保证。分配 volatile
或退出 synchronized
块时会跨越写入内存屏障,读取 volatile
或进入 synchronized
块时会跨越读取屏障。 Java 编译器无法重新排序通过这些内存屏障的写入指令,因此可以确保更新顺序。这些关键字控制指令重新排序并确保正确的内存同步。
注意: volatile
在单线程应用程序中是不必要的,因为程序顺序确保读写一致。单线程应用程序可能会在时间 3 和 4 看到(非易失性)a
和 b
的任何值,但它 总是 在时间 5 看到 3 因为语言保证。因此,尽管 volatile
的使用改变了单线程应用程序中的重新排序行为,但只有在线程之间共享数据时才需要它。
Java memory visibility documentation 表示:
A write to a volatile field happens-before every subsequent read of that same field.
我对后续在多线程上下文中的含义感到困惑。这句话是否暗示了所有处理器和内核的一些全局时钟。因此,例如,我在某个线程的循环 c1 中为变量赋值,然后第二个线程能够在后续循环 c1 + 1 中看到该值?
这意味着一旦某个线程写入到一个volatile字段,所有其他线程将观察(在下一次读取时)写入的值;但这并不能保护你免受种族歧视。
线程有自己的缓存,这些缓存将失效并通过缓存一致性协议更新为新写入的值。
编辑
后续意味着每当发生这种情况时在 写入本身之后。由于您不知道确切的 cycle/timing 何时会发生,您通常会说当某个其他线程观察到写入时,它将观察到该写入之前完成的所有操作;因此 volatile 建立了 happens-before 保证。
有点像示例:
// 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。
这更像是对不会发生什么而不是会发生什么的定义。
本质上是说,一旦 write 到 atomic
变量发生,就不会有任何其他线程在读取变量时读取陈旧的值。
考虑以下情况。
线程 A 正在连续递增
atomic
值a
。线程 B 偶尔读取
A.a
并将该值公开为 非原子b
变量。线程C偶尔会同时读取
A.a
和B.b
。
鉴于 a
是 atomic
可以推断,从 C 的角度来看,b
可能偶尔是小于 a
但 绝不会大于 a
.
如果 a
是 不是 原子,则无法提供此类保证。在某些缓存情况下,C 很可能随时看到 b
超过 a
的进度。
这是一个简单的演示,说明 Java 内存模型如何让您推理 在多线程环境中什么可以发生,什么不能发生。在现实生活中,读取和写入数据结构之间的潜在竞争条件可能要复杂得多,但推理过程是相同的。
在我看来,它在线程 之间提供无锁acquire/release 内存排序语义。请参阅 Jeff Preshing's article explaining the concept(主要针对 C++,但文章的要点是语言中立的并且是关于概念的。)
事实上Javavolatile
提供顺序一致性,而不仅仅是acq/rel。但是,没有实际的锁定。请参阅 Jeff Preshing 的文章,了解为什么命名与您对锁所做的操作相匹配。)
如果 a reader看到你写的值,那么它就知道生产者线程中的一切在写之前也已经发生了。
此排序保证仅与有关在单个线程内排序的其他保证结合使用。
例如
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
不是易变的,读取它不会在两个线程之间建立 happens-before 关系。
您不必有自旋循环,您可以从 volatile int
读取序列号或数组索引,然后读取 data[i]
.
我不太了解Java。我认为 volatile
实际上为您提供了顺序一致性,而不仅仅是 release/acquire。顺序释放存储不允许在以后的加载中重新排序,因此在典型的硬件上,它需要一个昂贵的内存屏障来确保在允许执行任何以后的加载之前刷新本地核心的存储缓冲区。
Volatile Vs Atomic 详细解释 volatile
给你的顺序。
Java volatile
只是一个排序关键字;它 not 等同于 C11 _Atomic
或 C++11 std::atomic<T>
,它们也为您提供原子 RMW 操作。在 Java 中,volatile_var++
是 而不是 原子增量,它是一个单独的加载和存储,如 volatile_var = volatile_var + 1
。在 Java 中,你需要像 AtomicInteger
这样的 class 来获得原子 RMW。
请注意 C/C++ volatile
根本不暗示原子性或顺序;它只是告诉编译器假设该值可以异步修改。除了最简单的情况,这只是你需要为任何东西编写无锁代码的一小部分。
在这个答案中,我试图仅使用 JLS 中定义的概念进行解释。
在 Java 中,每个线程都由一组 操作组成。
其中一些动作有可能被其他线程观察到(例如写一个共享变量),这些
称为 同步操作 。
线程的动作在源代码中的编写顺序称为程序顺序。
一个顺序定义什么是 before 和什么是 after(或者更好的是,not before)。
在一个线程中,每个动作与 next(按程序顺序)具有 happens-before 关系(用 < 表示)行动。
这种关系很重要,但很难理解,因为它非常基础:它保证如果 A < B 那么
A 的 "effects" 对 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 或交换质数的序列。
但是,每个这样的序列都必须让每个线程的单个操作根据线程的程序顺序进行排序。
这两个程序命令不足以命令每个动作,它们是部分命令,与 总订单 我们正在寻找。
根据可测量的时间(如时钟)将操作排成一行的总顺序称为执行顺序。
它是动作实际发生的顺序(只要求动作出现发生在
这个顺序,但这只是一个优化细节)。
到目前为止,这些操作没有在线程间(两个不同线程之间)排序。
同步操作就是为了这个目的。
每个同步动作 同步 至少另一个同步动作(它们通常成对出现,比如
volatile 变量的写入和读取,互斥量的锁定和解锁)。
synchronize-with关系是thread之间的happens-before(前者暗示后者),暴露为 一个不同的概念,因为 1) 它略微是 2) happens-before 在同步时由硬件自然强制执行 可能需要软件干预。
happens-before 源自程序顺序,synchronize-with 来自同步顺序(用<< 表示)。
同步顺序根据两个属性定义: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
全部都是同步命令,没有一个同步命令而是很多,上面的问题是错误的,它 应该是"What are the synchronization orders?"。
如果 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 的顺序。
同步操作受同步顺序影响,它们不会强加任何具体化。
一些同步操作(例如锁)强加了特定的执行顺序(因此是同步顺序)但有些则没有(例如 reads/writes of volatiles)。
创建同步顺序的是执行顺序,这与同步关系完全正交。
长话短说,"subsequent"形容词指的是任意同步顺序,即任意有效(根据每个线程 程序顺序)包含所有同步操作的顺序。
然后 JLS 继续定义数据竞争何时发生(当两个冲突的访问未按 happens-before 排序时)
以及发生之前一致的含义。
这些超出了范围。
I'm confused what does subsequent means in context of multithreading. Does this sentence implies some global clock for all processors and cores...?
随后的意思是(根据字典)及时之后。计算机中的所有 CPUs 肯定有一个全局时钟(想想 X Ghz)并且文档试图说如果线程 1 在时钟节拍 1 做某事然后线程 2 在另一个 CPU 在时钟滴答 2 时,它的动作被认为是后续的。
A write to a volatile field happens-before every subsequent read of that same field.
可以添加到这句话中以使其更清楚的关键短语是 "in another thread"。理解为:
可能更有意义A write to a volatile field happens-before every subsequent read of that same field in another thread.
这是什么意思 如果 在线程 1 中写入之后(及时)在线程 2 中读取 volatile
字段,则线程-2 将保证看到更新后的值。在 documentation you point to 的更上方是部分(强调我的):
... The results of a write by one thread are guaranteed to be visible to a read by another thread only if the write operation happens-before the read operation. The synchronized and volatile constructs, as well as the Thread.start() and Thread.join() methods, can form happens-before relationships. In particular.
注意突出显示的短语。 Java 编译器可以出于优化目的自由地在任何一个线程的执行中重新排序指令,只要重新排序不违反语言的定义——这称为 执行 顺序并且与程序顺序截然不同。
让我们看下面的示例,其中变量 a
和 b
是初始化为 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
。但是,如果线程 2 在时间 5 添加 x + y
,它可能会得到 0、1、2 或 3,具体取决于竞争条件。为什么?因为出于效率原因,编译器可能已将 Thread-1 中的指令重新排序以在 b
之后设置 a
。此外,Thread-1 可能没有适当发布 a
和 b
的值,因此 Thread-2 可能会得到过时的值。即使 Thread-1 被上下文切换或越过写入内存屏障并且 a
和 b
已 已发布,Thread-2 也需要越过读取屏障更新 a
和 b
.
如果 a
和 b
被标记为 volatile
那么对 a
的写入必须发生在(就可见性保证而言)后续读取 a
在第 3 行和对 b
的写入必须发生在后续读取 b
在第 4 行之前。两个线程都将获得 3.
我们在 java 中使用 volatile
和 synchronized
关键字来确保 happens-before 保证。分配 volatile
或退出 synchronized
块时会跨越写入内存屏障,读取 volatile
或进入 synchronized
块时会跨越读取屏障。 Java 编译器无法重新排序通过这些内存屏障的写入指令,因此可以确保更新顺序。这些关键字控制指令重新排序并确保正确的内存同步。
注意: volatile
在单线程应用程序中是不必要的,因为程序顺序确保读写一致。单线程应用程序可能会在时间 3 和 4 看到(非易失性)a
和 b
的任何值,但它 总是 在时间 5 看到 3 因为语言保证。因此,尽管 volatile
的使用改变了单线程应用程序中的重新排序行为,但只有在线程之间共享数据时才需要它。