直观地说,顺序一致性意味着多线程程序的执行看起来好像程序是按照原始程序顺序一次执行一条语句,即开发人员看到的代码中语句的实际顺序。顺序一致性是人们直觉地推理并发性的方式。
这里的重点是动词出现。事实上,编译器和 VM 可以自由地在后台执行许多优化,只要它们不破坏顺序一致性。
根据内存模型,只有正确同步的程序才会出现顺序一致。换句话说:如果一个程序没有正确同步,它在运行时的执行可能对应于您无法通过按原始程序顺序一次执行一条语句来实现的执行。
让我们考虑原始代码
T1 T2
a = 3 b = 5
b = 4 a = 6
顺序一致的执行可以是a=3,b=4,b=5,a=6
, or a=3,b=5,a=6,b=4
, or b=5,a=6,a=3,b=4
or or a=3,b=5,b=4,a=6
or b=5,a=3,b=4,a=6
or b=5,a=3,a=6,b=4
(所有可能的交错)
为了保证 JVM 中的顺序执行,您应该将四个分配中的每一个都包装在一个同步块中。否则,编译器和 VM 被授权进行可能破坏直观顺序一致性的优化。例如,他们可以决定将 T1 的语句重新排序为b=4,a=3
。代码不会以原始程序顺序运行。通过这种重新排序,可能会发生以下执行:b=4,b=5,a=6,a=3
,导致b=5,a=3
. 顺序一致性无法达到此状态。
如果程序没有正确同步,可能会破坏顺序一致性的原因是优化考虑了单个线程的一致性,而不是整个程序。在这里,如果孤立地考虑,交换 T1 中的分配不会损害 T1 的逻辑。然而,它损害了线程 T1 和 T2 交错的逻辑,因为它们改变了相同的变量,即它们具有数据竞争。如果分配被包装到同步块中,那么重新排序将是不合法的。
您的观察中确实有一些事实,即如果您不读取堆,您实际上不会注意到发生的竞争。但是,可以安全地假设写入的任何变量也同时被读取,否则它没有任何用途。正如这个小例子应该举例说明的那样,写入不应该竞争,否则它们可能会破坏堆,这可能会在以后产生意想不到的后果。
现在,更糟糕的是,JVM 上的读取和写入并不是原子操作(读取和写入双打需要内存访问)。因此,如果他们竞争,他们可以破坏堆,不仅在它不一致的意义上,而且在它包含从未真正存在的价值的意义上。
最后,赋值表达式的结果是赋值发生后变量的值。所以x = ( y = z )
是有效的。这假定写入没有与并发竞争竞争并返回写入的值。
简而言之:如果读写没有正确同步,就很难保证它们的效果。