澄清单一作家
Clarification on single writer
我对以下陈述进行了一些澄清(来源 - https://mechanical-sympathy.blogspot.in/2011/09/single-writer-principle.html):
'x86/x64 have a memory model, whereby load/store memory operations have preserved order, thus memory barriers are not required if you adhere strictly to the single writer principle.
On x86/x64 "loads can be re-ordered with older stores" according to the memory model so memory barriers are required when multiple threads mutate the same data across cores. '
这是否意味着:
1. 在单核内,load/store内存操作总是有序的?
因此,单个核心系统上的单个编写器线程(和多个 reader 线程)不需要 'synchronize' 来解决可见性问题?
2. 对于多核,负载可以通过其他核发起的存储重新排序 ?
因此,单个写入器线程(以及其他内核上的多个 reader 线程 运行 )不需要 'synchronize' 来解决可见性问题(因为不会有任何存储)?
因此,如果我们严格维护单个写入器 - 我们实际上可以取消在原始锁中对读取和写入都使用 'synchronized' 的做法。
我们真的可以完全取消 'synchronized' 吗?
在单核内,内存访问是按顺序还是乱序并不重要。如果只有一个核心,它将始终感知一致的值,因为读取请求将由保存尚未写入数据的同一缓存提供服务。
然而,这与 Java 程序无关,因为 Java 内存模型是 Java 语言规范的一部分,不为多线程程序提供此类保证.事实上,“内存屏障”一词根本没有出现在规范中。
您必须意识到,您编写的 Java 代码不会是 CPU 将执行的 x86/x64 代码。优化后的本机代码与您的源代码完全不同。代码优化的一个基本部分是消除冗余的读写甚至条件代码部分,前提是值不会在中间发生虚假变化,这对于单线程执行总是正确的。
如果由于没有适当的线程安全构造的多线程操作而导致基本假设无效,则此优化代码将产生不一致的结果。在规范中,这是公认的不一致,因为不惜一切代价强制执行一致结果的内存模型会导致性能极差。线程安全构造,如同步或易失性写入和读取,不仅告诉 JVM 在何处插入内存屏障(如果底层体系结构需要),而且还告知在哪里以及如何限制代码优化。
这就是为什么 a) 在操作可变共享状态时需要适当的线程安全构造和 b) 这些构造可能会降低性能,即使在 CPU/hardware 级别不需要内存屏障。
大免责声明
我在这里写的一些东西 实际上 测试过——比如重新排序、刷新等;其中一些花了很多时间阅读,希望我没看错。
所有东西 都重新排序,不重新排序并让你的程序 运行 保持原样的策略在多年前就被放弃了。只要输出不变,操作就会按需要重新排序。
例如:
static int sum(int x, int y){
x = x + 1;
y = y + 1;
return x + y;
}
你真的不在乎这些操作的顺序只要结果是正确的,对吗?
没有内存屏障(通常称为StoreLoad|StoreStore|LoadStore|LoadLoad
)任何操作都可以改变。为了保证一些操作不move beyond a fence
,有cpu fences
实现。 Java 生成它们的方法很少 - volatile
、synchroniztion
、Unsafe/VarHandle
(可能还有其他方法,我不知道)。
例如,基本上当您写入 volatile 时,会发生这种情况:
volatile x...
[StoreStore] - inserted by the compiler
[LoadStore]
x = 1; // volatile store
[StoreLoad]
...
[StoreLoad]
int t = x; // volatile load
[LoadLoad]
[LoadStore]
让我们举一个例子的子集:
[StoreStore]
[LoadStore]
x = 1; // volatile store
这意味着变量的anyStore
或Load
不能用x = 1
重新排序。同样的原则也适用于其他障碍。
Martin Thomson 说 on x86
4 个 屏障中有 3 个是免费的,唯一发行的是:StoreLoad
。它们是免费的,因为 x86 具有强大的内存模型,这意味着其他操作 默认情况下不会重新排序 。在其他 cpu 上,其中一些操作也很便宜(如果我在 ARM
上弄错了,那是 lwsync
- 轻量级同步;名称应该是不言自明的)。
此外,CPU 和缓存之间有一个小缓冲区 - 称为 Store Buffer
。当您向变量写入内容时,它不会直接进入缓存。它进入那个缓冲区。当它已满(或 被强制 通过 StoreLoad
耗尽)时,它会将写入内容放入缓存 - 由 cache coherency protocol
同步所有数据缓存。
Martin 所说的是,如果您有 多个 作者,您必须多次发布 StoreLoad
- 因此它很昂贵。如果您只有一个作家,则不必这样做。缓冲区满时将被排空。什么时候发生?好吧有时,理论上永远不会,实际上很快。
一些很棒的资源(这些资源有时让我整夜不眠,所以当心!):
这些 StoreStore
顺便说一句,每次你在构造函数中写入最终变量时:
private final int i ;
public MyObj(int i){
this.i = i;
// StoreStore here
}
我对以下陈述进行了一些澄清(来源 - https://mechanical-sympathy.blogspot.in/2011/09/single-writer-principle.html):
'x86/x64 have a memory model, whereby load/store memory operations have preserved order, thus memory barriers are not required if you adhere strictly to the single writer principle.
On x86/x64 "loads can be re-ordered with older stores" according to the memory model so memory barriers are required when multiple threads mutate the same data across cores. '
这是否意味着:
1. 在单核内,load/store内存操作总是有序的?
因此,单个核心系统上的单个编写器线程(和多个 reader 线程)不需要 'synchronize' 来解决可见性问题?
2. 对于多核,负载可以通过其他核发起的存储重新排序 ?
因此,单个写入器线程(以及其他内核上的多个 reader 线程 运行 )不需要 'synchronize' 来解决可见性问题(因为不会有任何存储)?
因此,如果我们严格维护单个写入器 - 我们实际上可以取消在原始锁中对读取和写入都使用 'synchronized' 的做法。 我们真的可以完全取消 'synchronized' 吗?
在单核内,内存访问是按顺序还是乱序并不重要。如果只有一个核心,它将始终感知一致的值,因为读取请求将由保存尚未写入数据的同一缓存提供服务。
然而,这与 Java 程序无关,因为 Java 内存模型是 Java 语言规范的一部分,不为多线程程序提供此类保证.事实上,“内存屏障”一词根本没有出现在规范中。
您必须意识到,您编写的 Java 代码不会是 CPU 将执行的 x86/x64 代码。优化后的本机代码与您的源代码完全不同。代码优化的一个基本部分是消除冗余的读写甚至条件代码部分,前提是值不会在中间发生虚假变化,这对于单线程执行总是正确的。
如果由于没有适当的线程安全构造的多线程操作而导致基本假设无效,则此优化代码将产生不一致的结果。在规范中,这是公认的不一致,因为不惜一切代价强制执行一致结果的内存模型会导致性能极差。线程安全构造,如同步或易失性写入和读取,不仅告诉 JVM 在何处插入内存屏障(如果底层体系结构需要),而且还告知在哪里以及如何限制代码优化。
这就是为什么 a) 在操作可变共享状态时需要适当的线程安全构造和 b) 这些构造可能会降低性能,即使在 CPU/hardware 级别不需要内存屏障。
大免责声明
我在这里写的一些东西 实际上 测试过——比如重新排序、刷新等;其中一些花了很多时间阅读,希望我没看错。
所有东西 都重新排序,不重新排序并让你的程序 运行 保持原样的策略在多年前就被放弃了。只要输出不变,操作就会按需要重新排序。
例如:
static int sum(int x, int y){
x = x + 1;
y = y + 1;
return x + y;
}
你真的不在乎这些操作的顺序只要结果是正确的,对吗?
没有内存屏障(通常称为StoreLoad|StoreStore|LoadStore|LoadLoad
)任何操作都可以改变。为了保证一些操作不move beyond a fence
,有cpu fences
实现。 Java 生成它们的方法很少 - volatile
、synchroniztion
、Unsafe/VarHandle
(可能还有其他方法,我不知道)。
例如,基本上当您写入 volatile 时,会发生这种情况:
volatile x...
[StoreStore] - inserted by the compiler
[LoadStore]
x = 1; // volatile store
[StoreLoad]
...
[StoreLoad]
int t = x; // volatile load
[LoadLoad]
[LoadStore]
让我们举一个例子的子集:
[StoreStore]
[LoadStore]
x = 1; // volatile store
这意味着变量的anyStore
或Load
不能用x = 1
重新排序。同样的原则也适用于其他障碍。
Martin Thomson 说 on x86
4 个 屏障中有 3 个是免费的,唯一发行的是:StoreLoad
。它们是免费的,因为 x86 具有强大的内存模型,这意味着其他操作 默认情况下不会重新排序 。在其他 cpu 上,其中一些操作也很便宜(如果我在 ARM
上弄错了,那是 lwsync
- 轻量级同步;名称应该是不言自明的)。
此外,CPU 和缓存之间有一个小缓冲区 - 称为 Store Buffer
。当您向变量写入内容时,它不会直接进入缓存。它进入那个缓冲区。当它已满(或 被强制 通过 StoreLoad
耗尽)时,它会将写入内容放入缓存 - 由 cache coherency protocol
同步所有数据缓存。
Martin 所说的是,如果您有 多个 作者,您必须多次发布 StoreLoad
- 因此它很昂贵。如果您只有一个作家,则不必这样做。缓冲区满时将被排空。什么时候发生?好吧有时,理论上永远不会,实际上很快。
一些很棒的资源(这些资源有时让我整夜不眠,所以当心!):
这些 StoreStore
顺便说一句,每次你在构造函数中写入最终变量时:
private final int i ;
public MyObj(int i){
this.i = i;
// StoreStore here
}