摘要:舉個例子,在多線程不使用環境中,每個線程會從主存中復制變量到緩存以提高性能。保證了變量的可見性關鍵字解決了變量的可見性問題。在多線程同時共享變量的情形下,關鍵字已不足以保證程序的并發性。
volatile 關鍵字能把 Java 變量標記成"被存儲到主存中"。這表示每一次讀取 volatile 變量都會訪問計算機主存,而不是 CPU 緩存。每一次對 volatile 變量的寫操作不僅會寫到 CPU 緩存,還會刷新到主存中。
實際上從 Java 5 開始,volatile 變量不僅會在讀寫操作時訪問主存,他還被賦予了更多含義。
Java volatile 關鍵字保證了線程對變量改動的可見性。
舉個例子,在多線程 (不使用 volatile) 環境中,每個線程會從主存中復制變量到 CPU 緩存 (以提高性能)。如果你有多個 CPU,不同線程也許會運行在不同的 CPU 上,并把主存中的變量復制到各自的 CPU 緩存中,像下圖畫的那樣
若果不使用 volatile 關鍵字,你無法保證 JVM 什么時候從主存中讀變量到 CPU cache,或把變量從 CPU cache 寫回主存。這會導致很多并發問題,我會在下面的小節中解釋。
想像一下這種情形,兩個或多個線程同時訪問一個共享對象,對象中包含一個用于計數的變量:
public class SharedObject { public int counter = 0; }
假設 Thread-1 會增加 counter 的值,而 Thread-1 和 Thread-2 會不時地讀取 counter 變量。在這種情形中,如果變量 counter 沒有被聲明成 volatile,就無法保證 counter 的值何時會 (被 Thread-1) 從 CPU cache 寫回到主存。結果導致 counter 在 CPU 緩存的值和主存中的不一致:
Thread-2 無法讀取到變量最新的值,因為 Thread-1 沒有把更新后的值寫回到主存中。這被稱作 "可見性" 問題,即其他線程對某線程更新操作不可見。
volatile 保證了變量的可見性volatile 關鍵字解決了變量的可見性問題。通過把變量 counter 聲明為 volatile,任何對 counter 的寫操作都會立即刷新到主存。同樣的,所有對 counter 的讀操作都會直接從主存中讀取。
public class SharedObject { public volatile int counter = 0; }
還是上面的情形,聲明 volatile 后,若 Thread-1 修改了 counter 則會立即刷新到主存中,Thread-2 從主存讀取的 counter 是 Thread-1 更新后的值,保證了 Thread-2 對變量的可見性。
volatile 完全可見性volatile 關鍵字的可見性生效范圍會超出 volatile 變量本身,這種完全可見性表現為以下兩個方面:
如果 Thread-A 對 volatile 變量進行寫操作,Thread-B 隨后該 volatile 變量進行讀操作,那么 (在 Thread-A 寫 volatile 變量之前的) 所有對 Thread-A 可見的變量,也會 (在 Thread-B 讀 volatile 變量之后) 對 Thread-B 可見。
當 Thread-A 讀一個 volatile 變量時,所有其他對 Thread-A 可見的變量也會重新從主存中讀一遍。
很抽象?讓我們舉例說明:
public class MyClass { private int years; private int months private volatile int days; public int totalDays() { int total = this.days; total += months * 30; total += years * 365; return total; } public void update(int years, int months, int days){ this.years = years; this.months = months; this.days = days; } }
上面的 update() 方法給三個變量賦值 (寫操作),其中只有 days 是 volatile 變量。完全可見性在這的含義是,當對 days 進行寫操作時,線程可見的其他變量 (在寫 days 之前的變量) 都會一同回寫到主存,也就是說變量 months 和 years 都會回寫到主存。
上面的 totalDays() 方法一開始就把 volatile 變量 days 讀取到局部變量 total 中,當讀取 days 時,變量 months 和 years (在讀 days 之后的變量) 同樣會從主存中讀取。所以通過上面的代碼,你能確保讀到最新的 days, months 和 years。
指令重排的困擾為了提高性能,JVM 和 CPU 會被允許對程序進行指令重排,只要重排的指令語義保持一致。舉個例子:
int a = 1; int b = 2; a++; b++;
上述指令可能被重排成如下形式,語義跟先前保持一致:
int a = 1; a++; int b = 2; b++;
然而,當你使用了 volatile 變量時,指令重排有時候會產生一些困擾。讓我們再看下面的例子:
public class MyClass { private int years; private int months private volatile int days; public void update(int years, int months, int days){ this.years = years; this.months = months; this.days = days; } }
update() 方法在寫變量 days 時,對變量 years 和 months 的寫操作同樣會刷新到主存中。但如果 JVM 執行了指令重排會發生什么情況?就像下面這樣:
public void update(int years, int months, int days){ this.days = days; this.months = months; this.years = years; }
當變量 days 發生改變時,months 和 years 仍然會回寫到主存中。但這一次,days 的更新發生在寫 months 和 years 之前,導致 months 和 years 的新值可能對其他線程不可見,使程序語義發生改變。對此 JVM 有現成的解決方法,我們會在下一小節討論這個問題。
volatile 的 Happen-before 機制為了解決指令重排帶來的困擾,Java volatile 關鍵字在可見性的基礎上提供了 happens-before 這種擔保機制。happens-before 保證了如下方面:
如果其他變量的讀寫操作原本發生在 volatile 變量寫操作之前,他們不能被指令重排到 volatile 變量的寫操作之后。注意,發生在 volatile 變量寫操作之后的讀寫操作仍然可以被指令重排到 volatile 變量寫操作之前。happen-after 重排到 (volatile 寫操作) 之前是允許的,但 happen-before 重排到之后是不允許的。
如果其他變量的讀寫操作原本發生在 volatile 變量讀操作之后,他們不能被指令重排到 volatile 變量的讀操作之前。注意,發生在 volatile 變量讀操作之前的讀操作仍然可以被指令重排到 volatile 變量讀操作之后。happen-before 重排到 (volatile 讀操作) 之后是允許的,但 happen-after 重排到之前是不允許的。
happens-before 機制確保了 volatile 的完全可見性 (其他文章用到了有序性這個詞)
volatile 并不總是行得通雖然關鍵字 volatile 保證了對 volatile 變量的讀寫操作會直接訪問主存,但在某些情況下把變量聲明為 volatile 還不足夠。
回顧之前舉過的例子 —— Thread-1 對共享變量 counter 進行寫操作,聲明 counter 為 volatile 并不足以保證 Thread-2 總是能讀到最新的值。
實際上,可能會有多個線程對同一個 volatile 變量進行寫操作,也會把正確的新值寫回到主存,只要這個新值不依賴舊值。但只要這個新值依賴舊值 (也就是說線程先會讀取 volatile 變量,基于讀取的值計算出一個新值,并把新值寫回到 volatile 變量),volatile 關鍵字不再能夠保證正確的可見性 (其他文章會把這稱為原子性)。
在多線程同時共享變量 counter 的情形下,volatile 關鍵字已不足以保證程序的并發性。設想一下:Thread-1 從主存中讀取了變量 counter = 0 到 CPU 緩存中,進行加 1 操作但還沒把更新后的值寫回到主存。Thread-2 同一時間從主存中讀取 counter (值仍為 0) 到他所在的 CPU 緩存中,同樣進行加 1 操作,也沒來得及回寫到主存。情形如下圖所示:
Thread-1 和 Thread-2 現在處于不同步的狀態。從語義上來說,counter 的值理應是 2,但變量 counter 在兩個線程所在 CPU 緩存中的值卻是 1,在主存中的值還是 0。即使線程都把 counter 回寫到主存中,counter 更新成1,語義上依然是錯的。(這種情況應該使用 synchronized 關鍵字保證線程同步)
什么時候使用 volatile像之前的例子所說:如果有兩個或多個線程同時對一個變量進行讀寫,使用 volatile 關鍵字是不夠用的,因為對 volatile 變量的讀寫并不會阻塞其他線程對該變量的讀寫。你需要使用 synchronized 關鍵字保證讀寫操作的原子性,或者使用 java.util.concurrent 包下的原子類型代替 synchronized 代碼塊,例如:AtomicLong, AtomicReference 等。
如果只有一個線程對變量進行讀寫操作,其他線程僅有讀操作,這時使用 volatile 關鍵字就能保證每個線程都能讀到變量的最新值,即保證了可見性。
volatile 的性能volatile 變量的讀寫操作會導致對主存的直接讀寫,對主存的直接訪問比訪問 CPU 緩存開銷更大。使用 volatile 變量一定程度上影響了指令重排,也會一定程度上影響性能。所以當迫切需要保證變量可見性的時候,你才會考慮使用 volatile。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/71947.html
摘要:三關鍵字能保證原子性嗎并發編程藝術這本書上說保證但是在自增操作非原子操作上不保證,多線程編程核心藝術這本書說不保證。多線程訪問關鍵字不會發生阻塞,而關鍵字可能會發生阻塞關鍵字能保證數據的可見性,但不能保證數據的原子性。 系列文章傳送門: Java多線程學習(一)Java多線程入門 Java多線程學習(二)synchronized關鍵字(1) java多線程學習(二)synchroniz...
摘要:變量可見性問題的關鍵字保證了多個線程對變量值變化的可見性。只要一個線程需要首先讀取一個變量的值,基于這個值生成一個新值,則一個關鍵字不足以保證正確的可見性。 Java的volatile關鍵字用于標記一個Java變量為在主存中存儲。更確切的說,對volatile變量的讀取會從計算機的主存中讀取,而不是從CPU緩存中讀取,對volatile變量的寫入會寫入到主存中,而不只是寫入到CPU緩存...
摘要:前半句是指線程內表現為串行的語義,后半句是指指令重排序現象和工作內存和主內存同步延遲現象。關于內存模型的講解請參考死磕同步系列之。目前國內市面上的關于內存屏障的講解基本不會超過這三篇文章,包括相關書籍中的介紹。問題 (1)volatile是如何保證可見性的? (2)volatile是如何禁止重排序的? (3)volatile的實現原理? (4)volatile的缺陷? 簡介 volatile...
摘要:前半句是指線程內表現為串行的語義,后半句是指指令重排序現象和工作內存和主內存同步延遲現象。關于內存模型的講解請參考死磕同步系列之。目前國內市面上的關于內存屏障的講解基本不會超過這三篇文章,包括相關書籍中的介紹。問題 (1)volatile是如何保證可見性的? (2)volatile是如何禁止重排序的? (3)volatile的實現原理? (4)volatile的缺陷? 簡介 volatile...
摘要:前半句是指線程內表現為串行的語義,后半句是指指令重排序現象和工作內存和主內存同步延遲現象。關于內存模型的講解請參考死磕同步系列之。目前國內市面上的關于內存屏障的講解基本不會超過這三篇文章,包括相關書籍中的介紹。問題 (1)volatile是如何保證可見性的? (2)volatile是如何禁止重排序的? (3)volatile的實現原理? (4)volatile的缺陷? 簡介 volatile...
閱讀 2683·2023-04-25 20:28
閱讀 1858·2021-11-22 09:34
閱讀 3693·2021-09-26 10:20
閱讀 1846·2021-09-22 16:05
閱讀 3090·2021-09-09 09:32
閱讀 2519·2021-08-31 09:40
閱讀 2103·2019-08-30 13:56
閱讀 3322·2019-08-29 17:01