一、問題
一下代碼經測試,打開注釋行,子線程就不會陷入while死循環了,為什么呢
public class VolatileTest3 { // b使用volatile修飾 public static volatile long b = 0; //消除緩存行的影響 public static long a1,a2,a3,a4,a5,a6,a7,a8; // c不使用volatile修飾 public static long c = 0; public static void main(String[] args) throws InterruptedException { new Thread(()->{ while (c == 0) { //long x = b; } System.out.println("c=" + c); }).start(); Thread.sleep(100); b = 1; c = 1; } }
可以理解為:如果不加volatile,java編程語言的java memory model允許一個線程讀到另一個線程任何一次寫進去的值(可以是初值0也可以是主線程寫入的1),只要不是happens-after它的就可以。但這個程序兩個線程沒有任何同步,所以沒有任何happens-before關系。所以,就算主線程寫,另一個線程永遠讀到c == 0,也是允許的。只要允許,你看到的“程序永遠退不出去”就是合理的結果。至於為什么會出現這種現象,你暫且認為是巧合吧,反正這是《Java語言標准》允許的,JVM沒做錯什么。
但是一旦加上volatile,所有線程對c的讀寫操作就構成一個序列。因為main早晚會執行完,所以早晚會又一個對c的寫操作,寫入1。由於new thread會不斷讀c,早晚會有一次讀happens after那個往c里寫1的操作。對於volatile變量來說,寫之后的讀都能看到那個寫的值“1”。所以那個new thread早晚可以看到c == 1。
或者可以理解為:共享變量c被兩個線程讀寫,cpu緩存行存在兩份值,如果不加volatile則更新過后的c的值不知道什么時候會更新到主存,加了volatile后會立即同步到主存,緩存行無效,另一個線程會重新從主存加載新值到cpu緩存行。
二、關於happens-before原則
Java內存模型中的happens-before是什么?為什么會有這東西的存在?
其實我們學習的初期或者時間很急迫的時候我們都是死記硬背,沒有理解這東西背后的含義和為什么需要這東西。沒辦法比如項目趕,一個新東西肯定是上手先,但是等我們空下來回過頭來,我們還是需要去理解這些知識,只有這樣我才能深刻的記住,並且運用熟練。
happens-before字面翻譯過來就是先行發生,A happens-before B 就是A先行發生於B?
不准確!在Java內存模型中,happens-before 應該翻譯成:前一個操作的結果可以被后續的操作獲取。講白點就是前面一個操作把變量a賦值為1,那后面一個操作肯定能知道a已經變成了1。
我們再來看看為什么需要這幾條規則?
因為我們現在電腦都是多CPU,並且都有緩存,導致多線程直接的可見性問題。詳情可以看我之前的文章面試官:你知道並發Bug的源頭是什么嗎?
所以為了解決多線程的可見性問題,就搞出了happens-before原則,讓線程之間遵守這些原則。編譯器還會優化我們的語句,所以等於是給了編譯器優化的約束。不能讓它優化的不知道東南西北了!
咱們來看看這幾條規則
程序次序規則:在一個線程內一段代碼的執行結果是有序的。就是還會指令重排,但是隨便它怎么排,結果是按照我們代碼的順序生成的不會變!
管程鎖定規則:就是無論是在單線程環境還是多線程環境,對於同一個鎖來說,一個線程對這個鎖解鎖之后,另一個線程獲取了這個鎖都能看到前一個線程的操作結果!(管程是一種通用的同步原語,synchronized就是管程的實現)
volatile變量規則:就是如果一個線程先去寫一個volatile變量,然后一個線程去讀這個變量,那么這個寫操作的結果一定對讀的這個線程可見。
線程啟動規則:在主線程A執行過程中,啟動子線程B,那么線程A在啟動子線程B之前對共享變量的修改結果對線程B可見。
線程終止規則:在主線程A執行過程中,子線程B終止,那么線程B在終止之前對共享變量的修改結果在線程A中可見。
線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程代碼檢測到中斷事件的發生,可以通過Thread.interrupted()檢測到是否發生中斷。
傳遞規則:這個簡單的,就是happens-before原則具有傳遞性,即A happens-before B , B happens-before C,那么A happens-before C。
對象終結規則:這個也簡單的,就是一個對象的初始化的完成,也就是構造函數執行的結束一定 happens-before它的finalize()方法。