Java中Volatile關(guān)鍵字詳解及代碼示例
一、基本概念
先補充一下概念:Java內(nèi)存模型中的可見性、原子性和有序性。
可見性:
可見性是一種復(fù)雜的屬性,因為可見性中的錯誤總是會違背我們的直覺。通常,我們無法確保執(zhí)行讀操作的線程能適時地看到其他線程寫入的值,有時甚至是根本不可能的事情。為了確保多個線程之間對內(nèi)存寫入操作的可見性,必須使用同步機制。
可見性,是指線程之間的可見性,一個線程修改的狀態(tài)對另一個線程是可見的。也就是一個線程修改的結(jié)果。另一個線程馬上就能看到。比如:用volatile修飾的變量,就會具有可見性。volatile修飾的變量不允許線程內(nèi)部緩存和重排序,即直接修改內(nèi)存。所以對其他線程是可見的。但是這里需要注意一個問題,volatile只能讓被他修飾內(nèi)容具有可見性,但不能保證它具有原子性。比如volatileinta=0;之后有一個操作a++;這個變量a具有可見性,但是a++依然是一個非原子操作,也就是這個操作同樣存在線程安全問題。
在Java中volatile、synchronized和final實現(xiàn)可見性。
原子性:
原子是世界上的最小單位,具有不可分割性。比如a=0;(a非long和double類型)這個操作是不可分割的,那么我們說這個操作時原子操作。再比如:a++;這個操作實際是a=a+1;是可分割的,所以他不是一個原子操作。非原子操作都會存在線程安全問題,需要我們使用同步技術(shù)(sychronized)來讓它變成一個原子操作。一個操作是原子操作,那么我們稱它具有原子性。java的concurrent包下提供了一些原子類,我們可以通過閱讀API來了解這些原子類的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
在Java中synchronized和在lock、unlock中操作保證原子性。
有序性:
Java語言提供了volatile和synchronized兩個關(guān)鍵字來保證線程之間操作的有序性,volatile是因為其本身包含“禁止指令重排序”的語義,synchronized是由“一個變量在同一個時刻只允許一條線程對其進行l(wèi)ock操作”這條規(guī)則獲得的,此規(guī)則決定了持有同一個對象鎖的兩個同步塊只能串行執(zhí)行。
下面內(nèi)容摘錄自《JavaConcurrencyinPractice》:
下面一段代碼在多線程環(huán)境下,將存在問題。
+ View code
/**
* @author zhengbinMac
*/
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
@Override
public void run() {
while(!ready) {
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
NoVisibility可能會持續(xù)循環(huán)下去,因為讀線程可能永遠都看不到ready的值。甚至NoVisibility可能會輸出0,因為讀線程可能看到了寫入ready的值,但卻沒有看到之后寫入number的值,這種現(xiàn)象被稱為“重排序”。只要在某個線程中無法檢測到重排序情況(即使在其他線程中可以明顯地看到該線程中的重排序),那么就無法確保線程中的操作將按照程序中指定的順序來執(zhí)行。當(dāng)主線程首先寫入number,然后在沒有同步的情況下寫入ready,那么讀線程看到的順序可能與寫入的順序完全相反。
在沒有同步的情況下,編譯器、處理器以及運行時等都可能對操作的執(zhí)行順序進行一些意想不到的調(diào)整。在缺乏足夠同步的多線程程序中,要想對內(nèi)存操作的執(zhí)行春旭進行判斷,無法得到正確的結(jié)論。
這個看上去像是一個失敗的設(shè)計,但卻能使JVM充分地利用現(xiàn)代多核處理器的強大性能。例如,在缺少同步的情況下,Java內(nèi)存模型允許編譯器對操作順序進行重排序,并將數(shù)值緩存在寄存器中。此外,它還允許CPU對操作順序進行重排序,并將數(shù)值緩存在處理器特定的緩存中。
二、Volatile原理
Java語言提供了一種稍弱的同步機制,即volatile變量,用來確保將變量的更新操作通知到其他線程。當(dāng)把變量聲明為volatile類型后,編譯器與運行時都會注意到這個變量是共享的,因此不會將該變量上的操作與其他內(nèi)存操作一起重排序。volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值。
在訪問volatile變量時不會執(zhí)行加鎖操作,因此也就不會使執(zhí)行線程阻塞,因此volatile變量是一種比sychronized關(guān)鍵字更輕量級的同步機制。

當(dāng)對非 volatile 變量進行讀寫的時候,每個線程先從內(nèi)存拷貝變量到CPU緩存中。如果計算機有多個CPU,每個線程可能在不同的CPU上被處理,這意味著每個線程可以拷貝到不同的 CPU cache 中。
而聲明變量是 volatile 的,JVM 保證了每次讀變量都從內(nèi)存中讀,跳過 CPU cache 這一步。
當(dāng)一個變量定義為 volatile 之后,將具備兩種特性:
1.保證此變量對所有的線程的可見性,這里的“可見性”,如本文開頭所述,當(dāng)一個線程修改了這個變量的值,volatile 保證了新值能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新。但普通變量做不到這點,普通變量的值在線程間傳遞均需要通過主內(nèi)存(詳見:Java內(nèi)存模型)來完成。
2.禁止指令重排序優(yōu)化。有volatile修飾的變量,賦值后多執(zhí)行了一個“l(fā)oad addl $0x0, (%esp)”操作,這個操作相當(dāng)于一個內(nèi)存屏障(指令重排序時不能把后面的指令重排序到內(nèi)存屏障之前的位置),只有一個CPU訪問內(nèi)存時,并不需要內(nèi)存屏障;(什么是指令重排序:是指CPU采用了允許將多條指令不按程序規(guī)定的順序分開發(fā)送給各相應(yīng)電路單元處理)。
volatile 性能:
volatile 的讀性能消耗與普通變量幾乎相同,但是寫操作稍慢,因為它需要在本地代碼中插入許多內(nèi)存屏障指令來保證處理器不發(fā)生亂序執(zhí)行。
volatile關(guān)鍵字代碼示例
volatile關(guān)鍵字的兩層語義
一旦一個共享變量(類的成員變量、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語義:
1)保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。
2)禁止進行指令重排序。
先看一段代碼,假如線程1先執(zhí)行,線程2后執(zhí)行:
//線程1
boolean stop = false;
while(!stop){
doSomething();
}
//線程2
stop = true;
這段代碼是很典型的一段代碼,很多人在中斷線程時可能都會采用這種標(biāo)記辦法。但是事實上,這段代碼會完全運行正確么?即一定會將線程中斷么?不一定,也許在大多數(shù)時候,這個代碼能夠把線程中斷,但是也有可能會導(dǎo)致無法中斷線程(雖然這個可能性很小,但是只要一旦發(fā)生這種情況就會造成死循環(huán)了)。
下面解釋一下這段代碼為何有可能導(dǎo)致無法中斷線程。在前面已經(jīng)解釋過,每個線程在運行過程中都有自己的工作內(nèi)存,那么線程1在運行的時候,會將stop變量的值拷貝一份放在自己的工作內(nèi)存當(dāng)中。
那么當(dāng)線程2更改了stop變量的值之后,但是還沒來得及寫入主存當(dāng)中,線程2轉(zhuǎn)去做其他事情了,那么線程1由于不知道線程2對stop變量的更改,因此還會一直循環(huán)下去。
但是用volatile修飾之后就變得不一樣了:
第一:使用volatile關(guān)鍵字會強制將修改的值立即寫入主存;
第二:使用volatile關(guān)鍵字的話,當(dāng)線程2進行修改時,會導(dǎo)致線程1的工作內(nèi)存中緩存變量stop的緩存行無效(反映到硬件層的話,就是CPU的L1或者L2緩存中對應(yīng)的緩存行無效);
第三:由于線程1的工作內(nèi)存中緩存變量stop的緩存行無效,所以線程1再次讀取變量stop的值時會去主存讀取。
那么在線程2修改stop值時(當(dāng)然這里包括2個操作,修改線程2工作內(nèi)存中的值,然后將修改后的值寫入內(nèi)存),會使得線程1的工作內(nèi)存中緩存變量stop的緩存行無效,然后線程1讀取時,發(fā)現(xiàn)自己的緩存行無效,它會等待緩存行對應(yīng)的主存地址被更新之后,然后去對應(yīng)的主存讀取最新的值。
那么線程1讀取到的就是最新的正確的值。
2.volatile保證原子性嗎?
從上面知道volatile關(guān)鍵字保證了操作的可見性,但是volatile能保證對變量的操作是原子性嗎?
下面看一個例子:
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完
Thread.yield();
System.out.println(test.inc);
}
}
大家想一下這段程序的輸出結(jié)果是多少?也許有些朋友認(rèn)為是10000。但是事實上運行它會發(fā)現(xiàn)每次運行結(jié)果都不一致,都是一個小于10000的數(shù)字。
可能有的朋友就會有疑問,不對啊,上面是對變量inc進行自增操作,由于volatile保證了可見性,那么在每個線程中對inc自增完之后,在其他線程中都能看到修改后的值啊,所以有10個線程分別進行了1000次操作,那么最終inc的值應(yīng)該是1000*10=10000。
這里面就有一個誤區(qū)了,volatile關(guān)鍵字能保證可見性沒有錯,但是上面的程序錯在沒能保證原子性??梢娦灾荒鼙WC每次讀取的是最新的值,但是volatile沒辦法保證對變量的操作的原子性。
在前面已經(jīng)提到過,自增操作是不具備原子性的,它包括讀取變量的原始值、進行加1操作、寫入工作內(nèi)存。那么就是說自增操作的三個子操作可能會分割開執(zhí)行,就有可能導(dǎo)致下面這種情況出現(xiàn):
假如某個時刻變量inc的值為10,
線程1對變量進行自增操作,線程1先讀取了變量inc的原始值,然后線程1被阻塞了;
然后線程2對變量進行自增操作,線程2也去讀取變量inc的原始值,由于線程1只是對變量inc進行讀取操作,而沒有對變量進行修改操作,所以不會導(dǎo)致線程2的工作內(nèi)存中緩存變量inc的緩存行無效,所以線程2會直接去主存讀取inc的值,發(fā)現(xiàn)inc的值時10,然后進行加1操作,并把11寫入工作內(nèi)存,最后寫入主存。
然后線程1接著進行加1操作,由于已經(jīng)讀取了inc的值,注意此時在線程1的工作內(nèi)存中inc的值仍然為10,所以線程1對inc進行加1操作后inc的值為11,然后將11寫入工作內(nèi)存,最后寫入主存。
那么兩個線程分別進行了一次自增操作后,inc只增加了1。
解釋到這里,可能有朋友會有疑問,不對啊,前面不是保證一個變量在修改volatile變量時,會讓緩存行無效嗎?然后其他線程去讀就會讀到新的值,對,這個沒錯。這個就是上面的happens-before規(guī)則中的volatile變量規(guī)則,但是要注意,線程1對變量進行讀取操作之后,被阻塞了的話,并沒有對inc值進行修改。然后雖然volatile能保證線程2對變量inc的值讀取是從內(nèi)存中讀取的,但是線程1沒有進行修改,所以線程2根本就不會看到修改的值。
根源就在這里,自增操作不是原子性操作,而且volatile也無法保證對變量的任何操作都是原子性的。
把上面的代碼改成以下任何一種都可以達到效果:
采用synchronized:
public class Test {
public int inc = 0;
public synchronized void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完
Thread.yield();
System.out.println(test.inc);
}
}
采用Lock:
public class Test {
public int inc = 0;
Lock lock = new ReentrantLock();
public void increase() {
lock.lock();
try {
inc++;
} finally{
lock.unlock();
}
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完
Thread.yield();
System.out.println(test.inc);
}
}
采用AtomicInteger:
public class Test {
public AtomicInteger inc = new AtomicInteger();
public void increase() {
inc.getAndIncrement();
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保證前面的線程都執(zhí)行完
Thread.yield();
System.out.println(test.inc);
}
}
總結(jié)
以上就是本文關(guān)于Java中Volatile關(guān)鍵字詳解及代碼示例的全部內(nèi)容,希望對大家有所幫助。感興趣的朋友可以繼續(xù)參閱本站:
如有不足之處,歡迎留言指出。
- 深入解析Java中volatile關(guān)鍵字的作用
- Java中volatile關(guān)鍵字的作用與用法詳解
- Java中volatile關(guān)鍵字實現(xiàn)原理
- java多線程編程之慎重使用volatile關(guān)鍵字
- java volatile關(guān)鍵字使用方法及注意事項
- 談?wù)凧ava中Volatile關(guān)鍵字的理解
- 詳解Java面試官最愛問的volatile關(guān)鍵字
- 詳解Java線程編程中的volatile關(guān)鍵字的作用
- Java里volatile關(guān)鍵字是什么意思
- Java中volatile關(guān)鍵字的作用是什么舉例詳解
相關(guān)文章
Java swing實現(xiàn)酒店管理系統(tǒng)
這篇文章主要為大家詳細介紹了Java swing實現(xiàn)酒店管理系統(tǒng),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-02-02
SpringBoot集成Swagger使用SpringSecurity控制訪問權(quán)限問題
這篇文章主要介紹了SpringBoot集成Swagger使用SpringSecurity控制訪問權(quán)限問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-05-05
SpringBoot啟動時自動執(zhí)行代碼的幾種實現(xiàn)方式
這篇文章主要給大家介紹了關(guān)于SpringBoot啟動時自動執(zhí)行代碼的幾種實現(xiàn)方式,文中通過實例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2022-02-02
POI讀取excel簡介_動力節(jié)點Java學(xué)院整理
這篇文章主要介紹了POI讀取excel簡介,詳細的介紹了什么是Apache POI和組件,有興趣的可以了解了解一下2017-08-08
Springboot應(yīng)用中線程池配置詳細教程(最新2021版)
這篇文章主要介紹了Springboot應(yīng)用中線程池配置教程(2021版),本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-03-03
struts2.5+框架使用通配符與動態(tài)方法常見問題小結(jié)
這篇文章主要介紹了struts2.5+框架使用通配符與動態(tài)方法常見問題 ,在文中給大家提到了Struts2.5框架使用通配符指定方法 ,需要的朋友可以參考下2018-09-09

