深入理解Java記憶體模型(JMM)中的可見性

可見性是指當一個執行緒修改了某個共享變數的值時,其他執行緒是否能夠立即知道這個修改。顯然,對於序列程式來說,可見性問題是不存在的。因為你在任何一個操作步驟中修改了某個變數的值,在後續的步驟中讀取這個變數的值時,讀取的一定是修改後的新值。

但是這個問題存在於並行程式中。如果一個執行緒修改了某個全域性變數的值,那麼其他執行緒未必可以馬上知道這個改動。圖1。14展示了發生可見性問題的一種可能。如果在CPU1和CPU2上各運行了一個執行緒,它們共享變數t,由於編譯器最佳化或者硬體最佳化的緣故,在CPU1上的執行緒將變數t進行了最佳化,然後將其快取在cache中或者暫存器裡。在這種情況下,如果在CPU2上的某個執行緒修改了變數t的實際值,那麼CPU1上的執行緒可能無法知道這個改動,依然會讀取cache中或者暫存器裡的資料。因此,就產生了可見性問題。外在表現為:變數t的值被修改,但是CPU1上的執行緒依然會讀到一箇舊值。可見性問題也是並行程式開發中需要重點關注的問題之一。

深入理解Java記憶體模型(JMM)中的可見性

圖1。14發生可見性問題的一種可能

可見性問題是一個綜合性問題。除上面提到的快取最佳化或者硬體最佳化(有些記憶體讀寫可能不會立即觸發,而會先進入一個硬體佇列等待)會導致可見性問題,指令重排(這個問題將在下一節中詳細討論)及編輯器的最佳化,也有可能導致一個執行緒的修改不會立即被其他執行緒察覺。

下面來看一個簡單的例子:

Thread 1 Thread 2

1: r2 = A; 3: r1 = B;

2: B = 1; 4: A = 2;

上述兩個執行緒並行執行,分別有1、2、3、4四條指令。其中指令1、2屬於執行緒1,而指令3、4屬於執行緒2。

從指令的執行順序上看,r2==2並且r1==1似乎是不可能出現的。但實際上,我們並沒有辦法從理論上保證這種情況不出現。因為編譯器可能將指令重排成:

Thread 1 Thread 2

B = 1; r1 = B;

r2 = A; A = 2;

在這種執行順序中,就有可能出現剛才看似不可能出現的r2 == 2並且r1 == 1的情況。

這個例子就說明,在一個執行緒中觀察另外一個執行緒的變數,它們的值是否能觀測到、何時能觀測到是沒有保證的。

再來看一個稍微複雜一些的例子:

Thread 1 Thread 2

r1 = p; r6 = p;

r2 = r1。x; r6。x = 3;

r3 = q;

r4 = r3。x;

r5 = r1。x;

這裡假設在初始時,p == q 並且 p。x == 0。對於大部分編譯器來說,可能會對執行緒1進行向前替換的最佳化,也就是r5 = r1。x這條指令會被直接替換成r5 = r2。由於它們都讀取了r1。x,又發生在同一個執行緒中,因此,編譯器很可能認為第2次讀取是完全沒有必要的。因此,上述指令可能會變成:

Thread 1 Thread 2

r1 = p; r6 = p;

r2 = r1。x; r6。x = 3;

r3 = q;

r4 = r3。x;

r5 = r2;

現在思考這麼一種場景。假設執行緒2中的r6。x = 3發生在r2 = r1。x和r4 = r3。x之間,而編譯器又打算重用r2來表示r5,那麼就有可能出現非常奇怪的現象。你看到的r2是0,r4是3,但是r5還是0。因此,如果從執行緒1來看就是:p。x的值從0變成了3(因為r4是3),接著又變成了0(這是不是算一個非常怪異的問題呢?)。

深入理解Java記憶體模型(JMM)中的可見性

內容摘自《實戰Java高併發程式設計(第3版)》,本書適合:所有java從業人員,所有資料庫工程師閱讀。

邏輯順暢。全書脈絡清晰,從Java高併發程式的設計基礎開始由底層原理落實到具體案例,環環相扣,完整流暢。結構嚴謹。總體上循序漸進,逐步提升。每一章都各自有鮮明的側重點,有利於讀者快速抓住重點。

實用性強。本書注重實戰,採用了理論結合實踐的編寫方法,給重要的知識點都安排了程式碼例項,幫助讀者在工作中實戰應用。