深入理解 Java 中的 volatile
Deep understanding of volatile in Java
Java是否允许输出1, 0
?我已经对其进行了非常深入的测试,但无法获得该输出。我只得到 1, 1
或 0, 0
或 0, 1
.
public class Main {
private int x;
private volatile int g;
// Executed by thread #1
public void actor1(){
x = 1;
g = 1;
}
// Executed by thread #2
public void actor2(){
put_on_screen_without_sync(g);
put_on_screen_without_sync(x);
}
}
为什么?
在我看来,有可能得到 1, 0
。我的推理。
g
是易失性的,因此会确保内存顺序。所以,它看起来像:
actor1:
(1) store(x, 1)
(2) store(g, 1)
(3) memory_barrier // on x86
而且,我看到了以下情况:
在 store(x,1)
之前重新排序 store(g, 1)
(memory_barrier 是 在 (2) 之后)。
现在,运行 线程 #2。所以,g = 1, x = 0
。现在,我们有了预期的输出。
我的推理有什么不正确的地方?
易失性写入之前的任何操作发生在 (HB) 同一变量的任何后续易失性读取之前。在您的情况下,写入 x
发生在写入 g
之前(由于程序顺序)。
所以只有三种可能:
- actor2 运行s first and x and g are 0 - output is 0,0
- actor1 运行s first 并且 x 和 g 是 1 因为 happens before 关系 HB - 输出是 1,1
- 方法 运行 并发并且只执行
x=1
(不是 g=1
)并且输出可以是 0,1 或 0,0(没有易失性写入,所以不能保证)
不,这不可能。根据 JMM,线程 1 在写入易失性字段时可见的任何内容在线程 2 读取该字段时都变得可见。
还有一个例子与你的相似provided here:
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
//uses x - guaranteed to see 42.
}
}
}
不是,实际上volatile
的这个属性是在类中使用的,就像ConcurrentHashMap
一样,实现了一个无锁的happy path,大致是这样的:
volatile int locked = 0;
...
void mutate() {
if (Unsafe.compareAndSwapInt(locked,0,1)) {
/*this isn't exactly how you call this method, but the point stands:
if we read 0, we atomically replace it with 1 and continue on the happy
path */
//we are happy
//so we mutate the structure and then
locked = 0;
} else {
//contended lock, we aren't happy
}
}
由于在易失性写入之前的写入不能在易失性写入之后重新排序,并且易失性读取之后的读取不能在易失性读取之前重新排序,所以这样的代码确实可以作为 "lockless locking"。
你永远不会看到 1, 0
,但正确解释这并不容易,具体而言。首先让我们把一些明显的东西拿出来。规范 says:
If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).
这意味着在写入线程侧,hb(x, g)
和读取侧hb(g, x)
。但仅此而已,如果您必须对每个线程进行推理 individually, as the chapter about Program order
says::
Among all the inter-thread actions performed by each thread t...
因此,如果您想象 运行 一次连接每个线程,那么 happens-before
对每个线程都是正确的。但你没有。您的演员(我确定您在那里使用 jcstress
)同时 运行。所以靠“程序顺序”来推理是不够的(也不正确)。
您现在需要以某种方式同步这两个动作 - 阅读 和写作。这里是 how the specification says it can be done:
A write to a volatile variable synchronizes-with all subsequent reads of v by any thread (where "subsequent" is defined according to the synchronization order).
并且later说:
If an action x synchronizes-with a following action y, then we also have hb(x, y).
如果现在将所有这些放在一起:
(hb) (hb) (hb)
write(x) ------> write(g) -------> read(g) -------> read(x)
这也被称为“传递”关闭program order
和synchronizes-with order
。由于每一步都有 hb
,因此根据规范不可能看到 1, 0
(一个活泼的阅读)。
Java是否允许输出1, 0
?我已经对其进行了非常深入的测试,但无法获得该输出。我只得到 1, 1
或 0, 0
或 0, 1
.
public class Main {
private int x;
private volatile int g;
// Executed by thread #1
public void actor1(){
x = 1;
g = 1;
}
// Executed by thread #2
public void actor2(){
put_on_screen_without_sync(g);
put_on_screen_without_sync(x);
}
}
为什么?
在我看来,有可能得到 1, 0
。我的推理。
g
是易失性的,因此会确保内存顺序。所以,它看起来像:
actor1:
(1) store(x, 1)
(2) store(g, 1)
(3) memory_barrier // on x86
而且,我看到了以下情况:
在 store(x,1)
之前重新排序 store(g, 1)
(memory_barrier 是 在 (2) 之后)。
现在,运行 线程 #2。所以,g = 1, x = 0
。现在,我们有了预期的输出。
我的推理有什么不正确的地方?
易失性写入之前的任何操作发生在 (HB) 同一变量的任何后续易失性读取之前。在您的情况下,写入 x
发生在写入 g
之前(由于程序顺序)。
所以只有三种可能:
- actor2 运行s first and x and g are 0 - output is 0,0
- actor1 运行s first 并且 x 和 g 是 1 因为 happens before 关系 HB - 输出是 1,1
- 方法 运行 并发并且只执行
x=1
(不是g=1
)并且输出可以是 0,1 或 0,0(没有易失性写入,所以不能保证)
不,这不可能。根据 JMM,线程 1 在写入易失性字段时可见的任何内容在线程 2 读取该字段时都变得可见。
还有一个例子与你的相似provided here:
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
//uses x - guaranteed to see 42.
}
}
}
不是,实际上volatile
的这个属性是在类中使用的,就像ConcurrentHashMap
一样,实现了一个无锁的happy path,大致是这样的:
volatile int locked = 0;
...
void mutate() {
if (Unsafe.compareAndSwapInt(locked,0,1)) {
/*this isn't exactly how you call this method, but the point stands:
if we read 0, we atomically replace it with 1 and continue on the happy
path */
//we are happy
//so we mutate the structure and then
locked = 0;
} else {
//contended lock, we aren't happy
}
}
由于在易失性写入之前的写入不能在易失性写入之后重新排序,并且易失性读取之后的读取不能在易失性读取之前重新排序,所以这样的代码确实可以作为 "lockless locking"。
你永远不会看到 1, 0
,但正确解释这并不容易,具体而言。首先让我们把一些明显的东西拿出来。规范 says:
If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).
这意味着在写入线程侧,hb(x, g)
和读取侧hb(g, x)
。但仅此而已,如果您必须对每个线程进行推理 individually, as the chapter about Program order
says::
Among all the inter-thread actions performed by each thread t...
因此,如果您想象 运行 一次连接每个线程,那么 happens-before
对每个线程都是正确的。但你没有。您的演员(我确定您在那里使用 jcstress
)同时 运行。所以靠“程序顺序”来推理是不够的(也不正确)。
您现在需要以某种方式同步这两个动作 - 阅读 和写作。这里是 how the specification says it can be done:
A write to a volatile variable synchronizes-with all subsequent reads of v by any thread (where "subsequent" is defined according to the synchronization order).
并且later说:
If an action x synchronizes-with a following action y, then we also have hb(x, y).
如果现在将所有这些放在一起:
(hb) (hb) (hb)
write(x) ------> write(g) -------> read(g) -------> read(x)
这也被称为“传递”关闭program order
和synchronizes-with order
。由于每一步都有 hb
,因此根据规范不可能看到 1, 0
(一个活泼的阅读)。