我最近在jcstress中偶然发现了这个例子:
@JCStressTest
@State
@Outcome(id = "10", expect = ACCEPTABLE, desc = "Boring")
@Outcome(id = {"0", "1"}, expect = FORBIDDEN, desc = "Boring")
@Outcome(id = {"9", "8", "7", "6", "5"}, expect = ACCEPTABLE, desc = "Okay")
@Outcome( expect = ACCEPTABLE_INTERESTING, desc = "Whoa")
public static class Volatiles {
volatile int x;
@Actor
void actor1() {
for (int i = 0; i < 5; i++) {
x++;
}
}
@Actor
void actor2() {
for (int i = 0; i < 5; i++) {
x++;
}
}
@Arbiter
public void arbiter(I_Result r) {
r.r1 = x;
}
}
作者强调,由于增量不是原子操作,因此不能期望每次循环迭代"丢失"一次增量更新。因此,除了0
和1
之外,所有结果(最多10
)都是允许的(并且确实发生了)。
我明白为什么不允许0
:在对象的默认值初始化和 JLS 中每个线程中的第一个操作之间有一个 HB 边缘。 JLS 17.4.4
默认值(零、假或空)写入每个变量时,将与每个线程中的第一个操作同步。
作者还解释了如何获得结果2
:
The most interesting result, "2" can be explained by this interleaving:
Thread 1: (0 ------ stalled -------> 1) (1->2)(2->3)(3->4)(4->5)
Thread 2: (0->1)(1->2)(2->3)(3->4) (1 -------- stalled ---------> 2)
我知道你不能像这样"扩展"上面的解释:
Thread 1: (0 --------- stalled -----------> 1) (1->2)(2->3)(3->4)(4->5)
Thread 2: (0->1)(1->2)(2->3)(3->4)(4->5)
因为它导致结果5
. 但是没有会产生1
的执行吗? 我绝对想不出任何。但为什么实际上没有呢?
首先,让我们记住volatile
在 Java 中是如何工作的。
在 Java 中,所有易失性读取和写入在运行时都按全局顺序发生(即 JLS 17.4.4 中的同步顺序).
此全局顺序的属性:
- 它保留了每个线程的易失性读/写顺序(就JLS 17.4.4而言,它与程序顺序一致)
- 每次执行一个:来自不同线程的操作可以在同一程序的不同执行中以不同的方式交错
易失性读取始终返回对此变量的最后一个(按此全局顺序)易失性写入(即在 JLS 17.4.4 中同步
)。其次,让我们澄清一下,"增量不是原子操作"意味着x++
由 3 个原子操作组成:
var temp = x; // volatile read of 'x' to local variable 'temp'
temp = temp + 1; // increment of local variable 'temp'
x = temp; // volatile write to `x`
最后,让我们重写
The most interesting result, "2" can be explained by this interleaving:
Thread 1: (0 ------ stalled -------> 1) (1->2)(2->3)(3->4)(4->5)
Thread 2: (0->1)(1->2)(2->3)(3->4) (1 -------- stalled ---------> 2)
以显示对x
的易失性读取和写入的方式:
Thread1 Thread2
r₁₀:0 | global
r₂₀:0 | order of
w₂₁:1 | volatile
r₂₂:1 | reads
w₂₃:2 | and
r₂₄:2 | writes
w₂₅:3 V
r₂₆:3
w₂₇:4
w₁₁:1
r₂₈:1
r₁₂:1
w₁₃:2
r₁₄:2
w₁₅:3
r₁₆:3
w₁₇:4
r₁₈:4
w₁₉:5
w₂₉:2
在此图上:
- 易失性读写的全局顺序从上到下
r:1
是x
的易失性读数,返回1
w:1
是1
到x
的易失性写入
请注意:
w
始终在同一线程中写入前一个r
的值,递增 1r
始终返回全局顺序中上一个w
写入的值
现在我们可以回答您的问题:
但是没有会产生
1
的执行吗?我绝对想不出任何。但为什么实际上没有呢?
查看全局顺序中的最后一次写入
w₂₉
:它总是写入(r₂₈
读取的值)+1
(顺便说一句,全局顺序中的最后一次写入将始终是 Thread1 中的最后一个写入w₁₉
或 Thread2 中的最后一个写入w₂₉
;在这种情况下,它是w₂₉
, 但对于w₁₉
推理是一样的)r₂₈
始终按全局顺序读取上一次写入,即:- 来自同一线程的任一
w₂₇
- 或者一些从 Thread1 写入
w₁ₓ
(如果运行时w₁ₓ
发生在w₂₇
和r₂₈
之间)
无论如何,
r₂₈
总是返回一些以前的非初始写入.
但是每个非初始写入总是至少写入1
.
这意味着w₂₉
总是至少写入2
。- 来自同一线程的任一