[英]Visibility of a read after synchronized block
JMM 是否保證在synchronized
塊之后對在另一個線程中讀取的變量進行synchronized
寫入的可見性? 這就是我的意思:
public class SynchronizedWriteRead {
private int a;
private int b;
public void writer() {
synchronized (this) {
a = 5;
b = 7;
}
}
public void reader() {
synchronized (this) {
int r1 = a; /* 5 */
}
int r2 = b; /* ? */
}
}
JMM 保證監視器上的解鎖發生在該監視器上的每個后續鎖定之前。 但我不確定它是否僅與synchronized
塊體有關。
最近我在 Java 中遇到了 Aleksey Shipilëv 的這篇文章 - 安全發布和安全初始化。 它說:
請注意,在Unsafe DCL存儲中進行
synchronized
是如何無濟於事的,這與外行人的看法相反,它以某種方式神奇地“刷新了緩存”或諸如此類的東西。 在讀取受保護的 state 時,如果沒有配對鎖,則無法保證在鎖保護寫入之前看到寫入。
所以這就是我問自己這個問題的原因。 我在JLS中找不到答案。
讓我們換一種說法。 有時你會捎帶一個volatile
的發生之前的保證,如下所示:
public class VolatileHappensBefore {
private int a; /* specifically non-volatile */
private volatile int b;
public void writer() {
a = 5;
b = 7;
}
public void reader() {
int r1 = b; /* 7 */
int r2 = a; /* 5 */
}
}
因為同一線程中的順序操作由happens-before 支持,並且happens-before 本身是可傳遞的,所以您可以保證看到這兩種寫入。
我可以以相同的方式使用synchronized
發生之前的保證嗎? 甚至可能像這樣(我已經放置了sync
變量以禁止編譯器/JVM刪除其他空的synchronized
塊):
public void writer() {
a = 5;
b = 7;
synchronized (this) {
sync = 1;
}
}
public void reader() {
synchronized (this) {
int r = sync;
}
int r1 = a; /* ? */
int r2 = b; /* ? */
}
你現在已經得到了答案,公平地說,這里的每個人都是正確的,我只想添加一個重要的規則。 它被稱為“在一致性之前發生”,它是這樣的:
因此,雖然公認的答案確實是正確的,但應該提到,為了在(3)
和(4)
之間創建發生前的邊緣, (4)
必須觀察(3)
所做的寫入。
在您的示例中:
public void reader() {
synchronized (this) {
int r = sync;
}
int r1 = a; /* ? */
int r2 = b; /* ? */
}
這意味着int r = sync;
不正確,您需要斷言您實際上看到sync
為1
(您已觀察到寫入)。 例如,這將創建所需的邊緣:
if (sync == 1) {
// guaranteed that r1=5 and r2=7
int r1 = a;
int r2 = b;
}
JMM 是否保證在
synchronized
塊之后對在另一個線程中讀取的變量進行synchronized
寫入的可見性?
我可以以相同的方式使用同步發生之前的保證嗎?
是的,是的。
synchronized
和volatile
提供了相同的可見性保證。
事實上,一個volatile
變量的行為就好像這個變量的每次讀取和寫入都發生在它自己的小synchronized
塊中。
在JLS條款中:
非正式地,如果沒有發生之前的命令以防止讀取,則允許讀取r查看寫入w的結果。
volatile
和synchronized
是建立前發生關系的一些方法:
- 監視器上的解鎖發生在該監視器上的每個后續鎖定之前。
- 對 volatile 字段(第 8.3.1.4 節)的寫入發生在對該字段的每次后續讀取之前。
- ...
Java 中安全發布和安全初始化的引用描述了以下情況:
synchronized
塊中初始化synchronized
塊的情況下讀取。在這種情況下,閱讀器線程可能會在初始化過程中看到 object。
這里需要注意的重要一點是, happens-before是一個傳遞關系。 因此,如果A
發生在B
和B
發生在C
之前,我們可以安全地得出A
發生在C
之前的結論。
現在讓我們看看有問題的代碼(為了清楚起見,我添加了注釋):
public void writer() {
a = 5; //1
b = 7; //2
synchronized (this) {
sync = 1;
} //3
}
public void reader() {
synchronized (this) { //4
int r = sync;
}
int r1 = a; //5
int r2 = b; //6
}
我們知道1
發生在2
之前和2
發生在3
之前,因為它們是由同一個線程執行的:
如果 x 和 y 是同一線程的操作,並且 x 在程序順序中位於 y 之前,則為 hb(x, y)。
我們也知道3
發生在4
之前:
監視器 m 上的解鎖操作與 m 上的所有后續鎖定操作同步(其中“后續”根據同步順序定義)。
如果動作 x 與后續動作 y 同步,那么我們也有 hb(x, y)。
最后,我們知道4
發生在5
之前和5
發生在6
之前,因為它們在同一個線程中執行。
如果 x 和 y 是同一線程的操作,並且 x 在程序順序中位於 y 之前,則為 hb(x, y)。
所以我們最終得到了從 1 到 6 的一系列發生之前的關系。因此, 1
發生在6
之前。
JMM 是否保證在同步塊之后對在另一個線程中讀取的變量進行同步寫入的可見性?
是的,由於其他答案中給出的原因。
但是有一個問題!
讓我們假設reader
調用跟在writer
調用之后,因此在writer
退出其同步塊和 reader 進入其塊之間之前發生了一個事件......並傳遞它退出塊並讀取b
。
問題是當reader
退出其塊時,不再保證b
上的互斥。 所以假設另一個線程立即獲取互斥鎖並修改b
。 由於沒有 HB 鏈將寫入b
與 read in reader
連接起來,因此無法保證reader
代碼實際看到的更新...或writer
之前寫入的值。
簡而言之,在推理並發算法時,需要考慮的不僅僅是HB 關系。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.