Java 线程:所有共享变量都应该是 Volatile 吗?
Java Threads: Should all shared variables be Volatile ?
我正在尝试了解多线程在 Java 中的工作原理。我了解 Volatile
和 Synchronization
之间的区别。
Volatile
与可见性有关,不保证同步。当我们使用多线程环境时,每个线程都会在它们正在处理的变量的本地缓存中创建自己的副本。更新此值时,更新首先发生在本地缓存副本中,而不是实际变量中。因此,其他线程不知道其他线程正在更改的值。这就是 volatile
出现的地方。可变字段立即写入主内存,并从主内存读取。
片段来自 Thinking In Java
-
Synchronization also causes flushing to main memory, so if a field is
completely guarded by synchronized methods or blocks, it is not
necessary to make it volatile.
It’s typically only safe to use volatile instead of synchronized if
the class has only one mutable field. Again, your first choice should
be to use the synchronized keyword—that’s the safest approach, and
trying to do anything else is risky.
但我的问题是,如果在同步块中,正在修改非易失性共享变量,其他线程是否会看到更新后的数据? (由于所讨论的变量是非易失性的,其他线程应该从缓存而不是主内存中读取陈旧数据)
如果上述问题的答案是NO
,那我是否可以得出结论,每次使用同步时,我是否应该确保共享变量必须标记为volatile
?
如果答案是 YES
,那是否意味着我总是可以使用 synchronization
而不是将共享变量标记为 volatile
?
p.s: 在问这个问题之前,我已经阅读了 Whosebug 和其他网站上的很多答案,但我找不到问题的答案。
稍微简化一下:
volatile
仅提供可见性:当您读取 volatile
变量时,您会得到两个保证:(1) 您会看到对变量的最新写入,即使它是在另一个线程中执行的,并且(2) volatile
之前的所有写入也是可见的。
synchronized
为您提供可见性和原子性 - 使用同一监视器从 synchronized
块观察在 synchronized
块中执行的操作的线程将看到所有这些操作或 [=其中 37=]。
所以要回答你的问题,不,如果一个变量是在 synchronized
块中写入的,你不需要标记它 volatile
,前提是你总是从synchronized
块使用相同的监视器。
这里有几个使用 volatile 的例子:
static class TestVolatile {
private int i = 0;
private volatile int v = 0;
void write() {
i = 5;
v = 7;
}
void read() {
//assuming write was called beforehand
print(i); //could be 0 or 5
print(v); //must be 7
print(i); //must be 5
}
void increment() {
i = i + 1; //if two threads call the method concurrently
//i could be incremented by 1 only, not 2: no atomicity
}
}
还有几个例子 synchronized
:
static class TestSynchronized {
private int i = 0;
private int j = 0;
void write() {
synchronized(this) {
i = 5;
j = 7;
}
}
void read_OK() {
synchronized(this) {
//assuming write was called beforehand
print(i); //must be 5
print(j); //must be 7
print(i); //must be 5
}
}
void read_NOT_OK() {
synchronized(new Object()) { //not the same monitor
//assuming write was called beforehand
print(i); //can be 0 or 5
print(j); //can be 0 or 7
}
}
void increment() {
synchronized(this) {
i = i + 1; //atomicity guarantees that if two threads call the method
//concurrently, i will be incremented twice
}
}
}
如果该变量在每次访问时都受到同一个监视器锁的保护,那么就没有必要使它变易变。
同步块做两件事:单线程访问受锁保护的区域和可见性效果。可见性效果意味着在受该锁保护时对该变量所做的任何更改对于进入使用它的区域(锁)的任何其他线程都是可见的。
JLS 在程序中的指令上定义了一个名为 "happens-before" 的关系。可以看到一个简短的版本in the documentation of java.util.concurrent
。
如果写 "happens-before" 读
则对变量的写操作被同一变量的读操作看到
现在,如果两个线程都只在同步块内访问该变量,那么从同步块退出可以保证其中发生的事情 "happens-before" 在同一个同步监视器的下一个锁定之后发生的任何事情。
因此,如果线程 A 在同步块 中写入变量 x
,而线程 B 在同步块 x
中读取变量 x
同一个监视器上的同步块,那么x
不需要是易失性的 - 写入"happened before" 读取及其结果将对线程 B 可见。
但是如果线程 B 在没有同步的情况下读取变量,那么即使线程 A 在同步内完成,也不能保证写入 "happens before",并且变量是不安全的 - 除非它是 volatile
.
因此,如果您确保所有访问(读取和写入)都在同一监视器上的同步块内,那么您可以依靠 "happens before" 关系使您的写入可见。
我正在尝试了解多线程在 Java 中的工作原理。我了解 Volatile
和 Synchronization
之间的区别。
Volatile
与可见性有关,不保证同步。当我们使用多线程环境时,每个线程都会在它们正在处理的变量的本地缓存中创建自己的副本。更新此值时,更新首先发生在本地缓存副本中,而不是实际变量中。因此,其他线程不知道其他线程正在更改的值。这就是 volatile
出现的地方。可变字段立即写入主内存,并从主内存读取。
片段来自 Thinking In Java
-
Synchronization also causes flushing to main memory, so if a field is completely guarded by synchronized methods or blocks, it is not necessary to make it volatile.
It’s typically only safe to use volatile instead of synchronized if the class has only one mutable field. Again, your first choice should be to use the synchronized keyword—that’s the safest approach, and trying to do anything else is risky.
但我的问题是,如果在同步块中,正在修改非易失性共享变量,其他线程是否会看到更新后的数据? (由于所讨论的变量是非易失性的,其他线程应该从缓存而不是主内存中读取陈旧数据)
如果上述问题的答案是NO
,那我是否可以得出结论,每次使用同步时,我是否应该确保共享变量必须标记为volatile
?
如果答案是 YES
,那是否意味着我总是可以使用 synchronization
而不是将共享变量标记为 volatile
?
p.s: 在问这个问题之前,我已经阅读了 Whosebug 和其他网站上的很多答案,但我找不到问题的答案。
稍微简化一下:
volatile
仅提供可见性:当您读取volatile
变量时,您会得到两个保证:(1) 您会看到对变量的最新写入,即使它是在另一个线程中执行的,并且(2)volatile
之前的所有写入也是可见的。synchronized
为您提供可见性和原子性 - 使用同一监视器从synchronized
块观察在synchronized
块中执行的操作的线程将看到所有这些操作或 [=其中 37=]。
所以要回答你的问题,不,如果一个变量是在 synchronized
块中写入的,你不需要标记它 volatile
,前提是你总是从synchronized
块使用相同的监视器。
这里有几个使用 volatile 的例子:
static class TestVolatile {
private int i = 0;
private volatile int v = 0;
void write() {
i = 5;
v = 7;
}
void read() {
//assuming write was called beforehand
print(i); //could be 0 or 5
print(v); //must be 7
print(i); //must be 5
}
void increment() {
i = i + 1; //if two threads call the method concurrently
//i could be incremented by 1 only, not 2: no atomicity
}
}
还有几个例子 synchronized
:
static class TestSynchronized {
private int i = 0;
private int j = 0;
void write() {
synchronized(this) {
i = 5;
j = 7;
}
}
void read_OK() {
synchronized(this) {
//assuming write was called beforehand
print(i); //must be 5
print(j); //must be 7
print(i); //must be 5
}
}
void read_NOT_OK() {
synchronized(new Object()) { //not the same monitor
//assuming write was called beforehand
print(i); //can be 0 or 5
print(j); //can be 0 or 7
}
}
void increment() {
synchronized(this) {
i = i + 1; //atomicity guarantees that if two threads call the method
//concurrently, i will be incremented twice
}
}
}
如果该变量在每次访问时都受到同一个监视器锁的保护,那么就没有必要使它变易变。
同步块做两件事:单线程访问受锁保护的区域和可见性效果。可见性效果意味着在受该锁保护时对该变量所做的任何更改对于进入使用它的区域(锁)的任何其他线程都是可见的。
JLS 在程序中的指令上定义了一个名为 "happens-before" 的关系。可以看到一个简短的版本in the documentation of java.util.concurrent
。
如果写 "happens-before" 读
则对变量的写操作被同一变量的读操作看到现在,如果两个线程都只在同步块内访问该变量,那么从同步块退出可以保证其中发生的事情 "happens-before" 在同一个同步监视器的下一个锁定之后发生的任何事情。
因此,如果线程 A 在同步块 中写入变量 x
,而线程 B 在同步块 x
中读取变量 x
同一个监视器上的同步块,那么x
不需要是易失性的 - 写入"happened before" 读取及其结果将对线程 B 可见。
但是如果线程 B 在没有同步的情况下读取变量,那么即使线程 A 在同步内完成,也不能保证写入 "happens before",并且变量是不安全的 - 除非它是 volatile
.
因此,如果您确保所有访问(读取和写入)都在同一监视器上的同步块内,那么您可以依靠 "happens before" 关系使您的写入可见。