在Java中,我們都知道關鍵字synchronized可以用于實現線程間的互斥,但我們卻常常忘記了它還有另外一個作用,那就是確保變量在內存的可見性 - 即當讀寫兩個線程同時訪問同一個變量時,synchronized用于確保寫線程更新變量后,讀線程再訪問該 變量時可以讀取到該變量最新的值。
比如說下面的例子:
public class NoVisibility {
private static boolean ready = false;
private static int number = 0;
private static class ReaderThread extends Thread {
@Override
public void run() {
while (!ready) {
Thread.yield(); //交出CPU讓其它線程工作
}
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
你認為讀線程會輸出什么? 42? 在正常情況下是會輸出42. 但是由于重排序問題,讀線程還有可能會輸出0 或者什么都不輸出。
我們知道,編譯器在將Java代碼編譯成字節碼的時候可能會對代碼進行重排序,而CPU在執行機器指令的時候也可能會對其指令進行重排序,只要重排序不會破壞程序的語義 -
在單一線程中,只要重排序不會影響到程序的執行結果,那么就不能保證其中的操作一定按照程序寫定的順序執行,即使重排序可能會對其它線程產生明顯的影響。
這也就是說,語句"ready=true"的執行有可能要優先于語句"number=42"的執行,這種情況下,讀線程就有可能會輸出number的默認值0.
而在Java內存模型下,重排序問題是會導致這樣的內存的可見性問題的。在Java內存模型下,每個線程都有它自己的工作內存(主要是CPU的cache或寄存器),它對變量的操作都在自己的工作內存中進行,而線程之間的通信則是通過主存和線程的工作內存之間的同步來實現的。
比如說,對于上面的例子而言,寫線程已經成功的將number更新為42,ready更新為true了,但是很有可能寫線程只同步了number到主存中(可能是由于CPU的寫緩沖導致),導致后續的讀線程讀取的ready值一直為false,那么上面的代碼就不會輸出任何數值。

而如果我們使用了synchronized關鍵字來進行同步,則不會存在這樣的問題,
public class NoVisibility {
private static boolean ready = false;
private static int number = 0;
private static Object lock = new Object();
private static class ReaderThread extends Thread {
@Override
public void run() {
synchronized (lock) {
while (!ready) {
Thread.yield();
}
System.out.println(number);
}
}
}
public static void main(String[] args) {
synchronized (lock) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
}
這個是因為Java內存模型對synchronized語義做了以下的保證,

即當ThreadA釋放鎖M時,它所寫過的變量(比如,x和y,存在它工作內存中的)都會同步到主存中,而當ThreadB在申請同一個鎖M時,ThreadB的工作內存會被設置為無效,然后ThreadB會重新從主存中加載它要訪問的變量到它的工作內存中(這時x=1,y=1,是ThreadA中修改過的最新的值)。通過這樣的方式來實現ThreadA到ThreadB的線程間的通信。
這實際上是JSR133定義的其中一條happen-before規則。JSR133給Java內存模型定義以下一組happen-before規則,
- 單線程規則:同一個線程中的每個操作都happens-before于出現在其后的任何一個操作。
- 對一個監視器的解鎖操作happens-before于每一個后續對同一個監視器的加鎖操作。
- 對volatile字段的寫入操作happens-before于每一個后續的對同一個volatile字段的讀操作。
- Thread.start()的調用操作會happens-before于啟動線程里面的操作。
- 一個線程中的所有操作都happens-before于其他線程成功返回在該線程上的join()調用后的所有操作。
- 一個對象構造函數的結束操作happens-before與該對象的finalizer的開始操作。
- 傳遞性規則:如果A操作happens-before于B操作,而B操作happens-before與C操作,那么A動作happens-before于C操作。
實際上這組happens-before規則定義了操作之間的內存可見性,如果A操作happens-before B操作,那么A操作的執行結果(比如對變量的寫入)必定在執行B操作時可見。
為了更加深入的了解這些happens-before規則,我們來看一個例子:
//線程A,B共同訪問的代碼
Object lock = new Object();
int a=0;
int b=0;
int c=0;
//線程A,調用如下代碼
synchronized(lock){
a=1; //1
b=2; //2
} //3
c=3; //4
//線程B,調用如下代碼
synchronized(lock){ //5
System.out.println(a); //6
System.out.println(b); //7
System.out.println(c); //8
}
我們假設線程A先運行,分別給a,b,c三個變量進行賦值(注:變量a,b的賦值是在同步語句塊中進行的),然后線程B再運行,分別讀取出這三個變量的值并打印出來。那么線程B打印出來的變量a,b,c的值分別是多少?
根據單線程規則,在A線程的執行中,我們可以得出1操作happens before于2操作,2操作happens before于3操作,3操作happens before于4操作。同理,在B線程的執行中,5操作happens before于6操作,6操作happens before于7操作,7操作happens before于8操作。而根據監視器的解鎖和加鎖原則,3操作(解鎖操作)是happens before 5操作的(加鎖操作),再根據傳遞性 規則我們可以得出,操作1,2是happens before 操作6,7,8的。
則根據happens-before的內存語義,操作1,2的執行結果對于操作6,7,8是可見的,那么線程B里,打印的a,b肯定是1和2. 而對于變量c的操作4,和操作8. 我們并不能根據現有的happens before規則推出操作4 happens before于操作8. 所以在線程B中,訪問的到c變量有可能還是0,而不是3.