您好,登錄后才能下訂單哦!
一、基本概念
先補充一下概念:Java內存模型中的可見性、原子性和有序性。
可見性:
可見性是一種復雜的屬性,因為可見性中的錯誤總是會違背我們的直覺。通常,我們無法確保執行讀操作的線程能適時地看到其他線程寫入的值,有時甚至是根本不可能的事情。為了確保多個線程之間對內存寫入操作的可見性,必須使用同步機制。
可見性,是指線程之間的可見性,一個線程修改的狀態對另一個線程是可見的。也就是一個線程修改的結果。另一個線程馬上就能看到。比如:用volatile修飾的變量,就會具有可見性。volatile修飾的變量不允許線程內部緩存和重排序,即直接修改內存。所以對其他線程是可見的。但是這里需要注意一個問題,volatile只能讓被他修飾內容具有可見性,但不能保證它具有原子性。比如volatileinta=0;之后有一個操作a++;這個變量a具有可見性,但是a++依然是一個非原子操作,也就是這個操作同樣存在線程安全問題。
在Java中volatile、synchronized和final實現可見性。
原子性:
原子是世界上的最小單位,具有不可分割性。比如a=0;(a非long和double類型)這個操作是不可分割的,那么我們說這個操作時原子操作。再比如:a++;這個操作實際是a=a+1;是可分割的,所以他不是一個原子操作。非原子操作都會存在線程安全問題,需要我們使用同步技術(sychronized)來讓它變成一個原子操作。一個操作是原子操作,那么我們稱它具有原子性。java的concurrent包下提供了一些原子類,我們可以通過閱讀API來了解這些原子類的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
在Java中synchronized和在lock、unlock中操作保證原子性。
有序性:
Java語言提供了volatile和synchronized兩個關鍵字來保證線程之間操作的有序性,volatile是因為其本身包含“禁止指令重排序”的語義,synchronized是由“一個變量在同一個時刻只允許一條線程對其進行lock操作”這條規則獲得的,此規則決定了持有同一個對象鎖的兩個同步塊只能串行執行。
下面內容摘錄自《JavaConcurrencyinPractice》:
下面一段代碼在多線程環境下,將存在問題。
+ 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可能會持續循環下去,因為讀線程可能永遠都看不到ready的值。甚至NoVisibility可能會輸出0,因為讀線程可能看到了寫入ready的值,但卻沒有看到之后寫入number的值,這種現象被稱為“重排序”。只要在某個線程中無法檢測到重排序情況(即使在其他線程中可以明顯地看到該線程中的重排序),那么就無法確保線程中的操作將按照程序中指定的順序來執行。當主線程首先寫入number,然后在沒有同步的情況下寫入ready,那么讀線程看到的順序可能與寫入的順序完全相反。
在沒有同步的情況下,編譯器、處理器以及運行時等都可能對操作的執行順序進行一些意想不到的調整。在缺乏足夠同步的多線程程序中,要想對內存操作的執行春旭進行判斷,無法得到正確的結論。
這個看上去像是一個失敗的設計,但卻能使JVM充分地利用現代多核處理器的強大性能。例如,在缺少同步的情況下,Java內存模型允許編譯器對操作順序進行重排序,并將數值緩存在寄存器中。此外,它還允許CPU對操作順序進行重排序,并將數值緩存在處理器特定的緩存中。
二、Volatile原理
Java語言提供了一種稍弱的同步機制,即volatile變量,用來確保將變量的更新操作通知到其他線程。當把變量聲明為volatile類型后,編譯器與運行時都會注意到這個變量是共享的,因此不會將該變量上的操作與其他內存操作一起重排序。volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值。
在訪問volatile變量時不會執行加鎖操作,因此也就不會使執行線程阻塞,因此volatile變量是一種比sychronized關鍵字更輕量級的同步機制。
當對非 volatile 變量進行讀寫的時候,每個線程先從內存拷貝變量到CPU緩存中。如果計算機有多個CPU,每個線程可能在不同的CPU上被處理,這意味著每個線程可以拷貝到不同的 CPU cache 中。
而聲明變量是 volatile 的,JVM 保證了每次讀變量都從內存中讀,跳過 CPU cache 這一步。
當一個變量定義為 volatile 之后,將具備兩種特性:
1.保證此變量對所有的線程的可見性,這里的“可見性”,如本文開頭所述,當一個線程修改了這個變量的值,volatile 保證了新值能立即同步到主內存,以及每次使用前立即從主內存刷新。但普通變量做不到這點,普通變量的值在線程間傳遞均需要通過主內存(詳見:Java內存模型)來完成。
2.禁止指令重排序優化。有volatile修飾的變量,賦值后多執行了一個“load addl $0x0, (%esp)”操作,這個操作相當于一個內存屏障(指令重排序時不能把后面的指令重排序到內存屏障之前的位置),只有一個CPU訪問內存時,并不需要內存屏障;(什么是指令重排序:是指CPU采用了允許將多條指令不按程序規定的順序分開發送給各相應電路單元處理)。
volatile 性能:
volatile 的讀性能消耗與普通變量幾乎相同,但是寫操作稍慢,因為它需要在本地代碼中插入許多內存屏障指令來保證處理器不發生亂序執行。
volatile關鍵字代碼示例
volatile關鍵字的兩層語義
一旦一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾之后,那么就具備了兩層語義:
1)保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。
2)禁止進行指令重排序。
先看一段代碼,假如線程1先執行,線程2后執行:
//線程1 boolean stop = false; while(!stop){ doSomething(); } //線程2 stop = true;
這段代碼是很典型的一段代碼,很多人在中斷線程時可能都會采用這種標記辦法。但是事實上,這段代碼會完全運行正確么?即一定會將線程中斷么?不一定,也許在大多數時候,這個代碼能夠把線程中斷,但是也有可能會導致無法中斷線程(雖然這個可能性很小,但是只要一旦發生這種情況就會造成死循環了)。
下面解釋一下這段代碼為何有可能導致無法中斷線程。在前面已經解釋過,每個線程在運行過程中都有自己的工作內存,那么線程1在運行的時候,會將stop變量的值拷貝一份放在自己的工作內存當中。
那么當線程2更改了stop變量的值之后,但是還沒來得及寫入主存當中,線程2轉去做其他事情了,那么線程1由于不知道線程2對stop變量的更改,因此還會一直循環下去。
但是用volatile修飾之后就變得不一樣了:
第一:使用volatile關鍵字會強制將修改的值立即寫入主存;
第二:使用volatile關鍵字的話,當線程2進行修改時,會導致線程1的工作內存中緩存變量stop的緩存行無效(反映到硬件層的話,就是CPU的L1或者L2緩存中對應的緩存行無效);
第三:由于線程1的工作內存中緩存變量stop的緩存行無效,所以線程1再次讀取變量stop的值時會去主存讀取。
那么在線程2修改stop值時(當然這里包括2個操作,修改線程2工作內存中的值,然后將修改后的值寫入內存),會使得線程1的工作內存中緩存變量stop的緩存行無效,然后線程1讀取時,發現自己的緩存行無效,它會等待緩存行對應的主存地址被更新之后,然后去對應的主存讀取最新的值。
那么線程1讀取到的就是最新的正確的值。
2.volatile保證原子性嗎?
從上面知道volatile關鍵字保證了操作的可見性,但是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) //保證前面的線程都執行完 Thread.yield(); System.out.println(test.inc); } }
大家想一下這段程序的輸出結果是多少?也許有些朋友認為是10000。但是事實上運行它會發現每次運行結果都不一致,都是一個小于10000的數字。
可能有的朋友就會有疑問,不對啊,上面是對變量inc進行自增操作,由于volatile保證了可見性,那么在每個線程中對inc自增完之后,在其他線程中都能看到修改后的值啊,所以有10個線程分別進行了1000次操作,那么最終inc的值應該是1000*10=10000。
這里面就有一個誤區了,volatile關鍵字能保證可見性沒有錯,但是上面的程序錯在沒能保證原子性。可見性只能保證每次讀取的是最新的值,但是volatile沒辦法保證對變量的操作的原子性。
在前面已經提到過,自增操作是不具備原子性的,它包括讀取變量的原始值、進行加1操作、寫入工作內存。那么就是說自增操作的三個子操作可能會分割開執行,就有可能導致下面這種情況出現:
假如某個時刻變量inc的值為10,
線程1對變量進行自增操作,線程1先讀取了變量inc的原始值,然后線程1被阻塞了;
然后線程2對變量進行自增操作,線程2也去讀取變量inc的原始值,由于線程1只是對變量inc進行讀取操作,而沒有對變量進行修改操作,所以不會導致線程2的工作內存中緩存變量inc的緩存行無效,所以線程2會直接去主存讀取inc的值,發現inc的值時10,然后進行加1操作,并把11寫入工作內存,最后寫入主存。
然后線程1接著進行加1操作,由于已經讀取了inc的值,注意此時在線程1的工作內存中inc的值仍然為10,所以線程1對inc進行加1操作后inc的值為11,然后將11寫入工作內存,最后寫入主存。
那么兩個線程分別進行了一次自增操作后,inc只增加了1。
解釋到這里,可能有朋友會有疑問,不對啊,前面不是保證一個變量在修改volatile變量時,會讓緩存行無效嗎?然后其他線程去讀就會讀到新的值,對,這個沒錯。這個就是上面的happens-before規則中的volatile變量規則,但是要注意,線程1對變量進行讀取操作之后,被阻塞了的話,并沒有對inc值進行修改。然后雖然volatile能保證線程2對變量inc的值讀取是從內存中讀取的,但是線程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) //保證前面的線程都執行完 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) //保證前面的線程都執行完 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) //保證前面的線程都執行完 Thread.yield(); System.out.println(test.inc); } }
總結
以上就是本文關于Java中Volatile關鍵字詳解及代碼示例的全部內容,希望對大家有所幫助。感興趣的朋友可以繼續參閱本站:
淺談Java編程中的synthetic關鍵字
如有不足之處,歡迎留言指出。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。