java關鍵字Volatile用于將java變量標記為存儲在主內(nèi)存中,這就意味著每次讀取Volatile修飾的變量時都是從計算機的主內(nèi)存中讀取乃沙,而不是從CPU的緩存中讀取,并且每次對Volatile變量寫的時候都將寫入主內(nèi)存吼渡,而不僅僅是CPU緩存
可見性問題
Java關鍵字Volatile保證可以跨線程查看變量的變化,下面詳細來說一下這個問題
在線程操作非Volatile變量的多線程應用程序中壁却,出于性能原因,每個線程可以在處理它們時將變量從主內(nèi)存復制到CPU高速緩存中寻定。如果您的計算機包含多個CPU,則每個線程可以在不同的CPU上運行精耐。這意味著狼速,每個線程可以將變量復制到不同CPU的CPU緩存中。這在這里說明:
對于非Volatile變量卦停,java虛擬機從主內(nèi)存讀取到CPU緩存向胡,或從CPU緩存讀取到主內(nèi)存,這可能導致一系列問題:
假設兩個線程或者多個線程訪問一個共享對象惊完,共享對象包含一個counter變量僵芹,像這樣:
public class SharedObject {
public int counter = 0;
}
假設只有線程1遞增counter變量,但線程1和線程2都可能時不時讀取counter變量小槐。
如果counter變量沒有聲明為Volatile拇派,則無法保證counter從CPU緩存讀取到主內(nèi)存中的具體時間,這就意味著CPU緩存中的變量值可能跟主內(nèi)存中變量值不同,這種情況如下所示:
因為還沒有被線程寫入主線程凿跳,而導致另一個線程沒有看到變量的最新值得問題件豌,被稱為線程的“可見性”問題
,線程的更新操作對其他線程不可見
Java Volatile可見性保證
java Volatile關鍵幀意在解決線程的可見性問題控嗜,通過對counter聲明volatile茧彤,對象counter所有寫的操作將立即寫入主內(nèi)存,同時對counter的讀操作也是直接訪問主內(nèi)存
public class SharedObject {
public volatile int counter = 0;
}
因此聲明一個Volatile疆栏,可以保證對其他線程的可見性
在上面給出的場景中曾掂,一個線程(T1)修改計數(shù)器,另一個線程(T2)讀取計數(shù)器(但從不修改它)壁顶,聲明了volatile的counter足以保證T2對counter變量寫入的可見性珠洗。
但是,如果T1和T2都在增加counter變量若专,那么 counter變量聲明volatile就不夠了险污。稍后會詳細介紹。
volatile完全可見性保證
實際上富岳,volatile的可見性保證超出了volatile變量本身,可見性保證如下:
1.如果線程A 對volatile變量進行寫操作蛔糯,那么線程B可以立刻讀取相同的volatile變量,在對volatile變量寫前窖式,所有的變量對線程A都是可見的蚁飒。在讀取volatile變量后對線程B同樣是可見的。
2.如果線程A讀取volatile變量萝喘,則讀取變量時線程A的所有可見volatile變量也將從主內(nèi)存重新讀取
用實例代碼來說明:
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用了volatile
volatile完全可見性意味著琼懊,當對days進行寫入時,線程所有的可見變量都會寫入主內(nèi)存(不僅僅是volatile變量自己寫入到主存中爬早,其他被該線程修改的所有變量也會刷新到主存)哼丈,這就意味著,當對days進行寫入時筛严,years和months也將寫入主內(nèi)存,
當對years醉旦、months和days讀取時,你可以這么做
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;
}
注意totalDays從讀取days的值開始到獲取total桨啃,當讀取days時车胡,years和months也會讀取到主內(nèi)存中,因此可以保證讀取到days照瘾、years和months的最新值
指令重排
只要指令的語義含義不變匈棘,jvm和CPU就可以出于性能的原因,重新排序指令的程序析命,看以下說明:
int a = 1;
int b = 2;
a++;
b++;
這些指令可以按一下方式重排主卫,而不會丟失程序的語義含義:
int a = 1;
a++;
int b = 2;
b++;
然而,當程序中有一個volatile字段時鹃愤,指令重新排序提出了挑戰(zhàn)队秩。讓我們從MyClass類中看看一下java 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也會被寫入主內(nèi)存中昼浦,但是如果jvm把指令重排序了
public void update(int years, int months, int days){
this.days = days;
this.months = months;
this.years = years;
}
當days值被修改時馍资,months和years仍然被寫入主內(nèi)存,但是這一次days的值的改變在months和years寫入之前关噪,因此新值沒法正確的對其他線程可見鸟蟹,指令重排的語義以前被改變
Java volatile Happens-Before 保證
happens-before 關系是程序語句之間的排序保證,這能確保任何內(nèi)存的寫使兔,對其他語句都是可見的建钥。
為了解決指令重排挑戰(zhàn),volatile除了可見性保證之外虐沥,Java 關鍵字還提供“Happens-Before Guarantee”規(guī)則熊经。"Happens-Before"保證:
如果線程A寫入一個volatile變量,隨后線程B讀取相同的變量欲险。那么變量對線程A來說在寫入變量前就是可見的镐依,對于B來說讀取完變量后,對該變量也是可見的天试。
volatile變量的讀和寫指令不能由JVM重新排序()槐壳。讀寫指令前后可以重排序,但是volatile讀和寫不能與這些指令混合喜每。無論什么指令都應該在volatile變量讀寫之后务唐。
volatile并不足夠解決所有問題
volatile雖然能滿足直接把數(shù)據(jù)寫入主內(nèi)存并且直接從主內(nèi)存中取出雳攘,仍然存在不足的情況
在前面解釋的情況中,只有線程1寫入共享counter變量枫笛,聲明該counter變量volatile足以確保線程2始終看到最新的寫入值吨灭。
實際上,如果寫入volatile變量的新值不依賴于其先前的值刑巧,則多個線程甚至可以寫入共享變量喧兄,并且仍然具有存儲在主存儲器中的正確值。換句話說海诲,如果將值寫入共享volatile變量的線程首先不需要讀取其值來計算其下一個值繁莹。
一旦線程需要首先讀取volatile
變量的值檩互,并且基于該值為共享volatile
變量生成新值特幔,volatile
變量就不再足以保證正確的可見性。讀取volatile
變量和寫入新值之間的短時間間隔會產(chǎn)生競爭條件 闸昨,其中多個線程可能讀取volatile
變量的相同值蚯斯,為變量生成新值,并在將值寫回時主存 - 覆蓋彼此的值饵较。
多個線程遞增相同計數(shù)器的情況恰好是 volatile
變量不夠的情況拍嵌。以下部分更詳細地解釋了這種情況。
想象一下循诉,如果線程1將counter
值為0 的共享變量讀入其CPU高速緩存横辆,則將其增加到1并且不將更改的值寫回主存儲器。然后茄猫,線程2可以counter
從主存儲器讀取相同的變量狈蚤,其中變量的值仍為0,進入其自己的CPU高速緩存划纽。然后脆侮,線程2也可以將計數(shù)器遞增到1,也不將其寫回主存儲器勇劣。這種情況如下圖所示:
線程1和線程2現(xiàn)在幾乎不同步靖避。共享counter變量的實際值應為2,但每個線程的CPU緩存中的變量值為1比默,而主存中的值仍為0.這是一個混亂幻捏!即使線程最終將共享counter變量的值寫回主存儲器,該值也將是錯誤的命咐。
volatile在什么時候使用
正如我前面提到的粘咖,如果兩個線程都在讀取和寫入共享變量,那么使用 volatile
關鍵字是不夠的侈百。 在這種情況下瓮下,您需要使用synchronized來保證變量的讀取和寫入是原子性翰铡。讀取或?qū)懭雟olatile變量不會阻止線程讀取或?qū)懭搿榇朔砘担仨氃陉P鍵部分周圍使用synchronized
關鍵字锭魔。
作為synchronized
塊的替代方法,您還可以使用java.util.concurrent
包中找到的眾多原子數(shù)據(jù)類型之一路呜。例如迷捧,AtomicLong
或者 AtomicReference
其他更多。
如果只有一個線程讀取和寫入volatile變量的值胀葱,而其他線程只讀取變量漠秋,那么讀取線程將保證看到寫入volatile變量的最新值。則可以使用volatile關鍵詞
該volatile
關鍵字適用于32位和64位變量抵屿。
volatile的性能因素
volatile變量會導致變量讀取和寫入主內(nèi)存庆锦。讀取和寫入主內(nèi)存比訪問CPU緩存更昂貴。訪問volatile變量也會阻止指令重新排序轧葛,這是一種正常的性能增強技術搂抒。因此,在真正需要強制實施變量可見性時尿扯,應該只使用volatile變量求晶。