一,volatile關(guān)鍵字的可見性 要想理解volatile關(guān)鍵字,得先了解下JAVA的內(nèi)存模型,Java內(nèi)存模型的抽象示意圖如下: 從圖中可以看出: ①每個線程都有一個自己的本地內(nèi)存空間--線程??臻g???線程執(zhí)行時,先把變量從主內(nèi)存讀取到線程自己的本地內(nèi)存空間,然后再對該變量進行操作 ②對該變量操作完后,在某個時間再把變量刷新回主內(nèi)存 關(guān)于JAVA內(nèi)存模型,更詳細(xì)的可參考: 深入理解Java內(nèi)存模型(一)——基礎(chǔ) 因此,就存在內(nèi)存可見性問題,看一個示例程序:(摘自書上) 1 public class RunThread extends Thread { 2 3 private boolean isRunning = true; 4 5 public boolean isRunning() { 6 return isRunning; 7 } 8 9 public void setRunning(boolean isRunning) { 10 this.isRunning = isRunning; 11 } 12 13 @Override 14 public void run() { 15 System.out.println("進入到run方法中了"); 16 while (isRunning == true) { 17 } 18 System.out.println("線程執(zhí)行完成了"); 19 } 20 } 21 22 public class Run { 23 public static void main(String[] args) { 24 try { 25 RunThread thread = new RunThread(); 26 thread.start(); 27 Thread.sleep(1000); 28 thread.setRunning(false); 29 } catch (InterruptedException e) { 30 e.printStackTrace(); 31 } 32 } 33 } Run.java 第28行,main線程 將啟動的線程RunThread中的共享變量設(shè)置為false,從而想讓RunThread.java 第14行中的while循環(huán)結(jié)束。 如果,我們使用JVM -server參數(shù)執(zhí)行該程序時,RunThread線程并不會終止!從而出現(xiàn)了死循環(huán)!! 原因分析: 現(xiàn)在有兩個線程,一個是main線程,另一個是RunThread。它們都試圖修改 第三行的 isRunning變量。按照J(rèn)VM內(nèi)存模型,main線程將isRunning讀取到本地線程內(nèi)存空間,修改后,再刷新回主內(nèi)存。 而在JVM 設(shè)置成 -server模式運行程序時,線程會一直在私有堆棧中讀取isRunning變量。因此,RunThread線程無法讀到main線程改變的isRunning變量 從而出現(xiàn)了死循環(huán),導(dǎo)致RunThread無法終止。這種情形,在《Effective JAVA》中,將之稱為“活性失敗” 解決方法,在第三行代碼處用 volatile 關(guān)鍵字修飾即可。這里,它強制線程從主內(nèi)存中取 volatile修飾的變量。 volatile private boolean isRunning = true;
擴展一下,當(dāng)多個線程之間需要根據(jù)某個條件確定 哪個線程可以執(zhí)行時,要確保這個條件在 線程 之間是可見的。因此,可以用volatile修飾。 綜上,volatile關(guān)鍵字的作用是:使變量在多個線程間可見(可見性)
二,volatile關(guān)鍵字的非原子性 所謂原子性,就是某系列的操作步驟要么全部執(zhí)行,要么都不執(zhí)行。 比如,變量的自增操作 i++,分三個步驟: ①從內(nèi)存中讀取出變量 i 的值 ②將 i 的值加1 ③將 加1 后的值寫回內(nèi)存 這說明 i++ 并不是一個原子操作。因為,它分成了三步,有可能當(dāng)某個線程執(zhí)行到了第②時被中斷了,那么就意味著只執(zhí)行了其中的兩個步驟,沒有全部執(zhí)行。 關(guān)于volatile的非原子性,看個示例: 1 public class MyThread extends Thread { 2 public volatile static int count; 3 4 private static void addCount() { 5 for (int i = 0; i < 100; i++) { 6 count++; 7 } 8 System.out.println("count=" + count); 9 } 10 11 @Override 12 public void run() { 13 addCount(); 14 } 15 } 16 17 public class Run { 18 public static void main(String[] args) { 19 MyThread[] mythreadArray = new MyThread[100]; 20 for (int i = 0; i < 100; i++) { 21 mythreadArray[i] = new MyThread(); 22 } 23 24 for (int i = 0; i < 100; i++) { 25 mythreadArray[i].start(); 26 } 27 } 28 } MyThread類第2行,count變量使用volatile修飾 Run.java 第20行 for循環(huán)中創(chuàng)建了100個線程,第25行將這100個線程啟動去執(zhí)行 addCount(),每個線程執(zhí)行100次加1 期望的正確的結(jié)果應(yīng)該是 100*100=10000,但是,實際上count并沒有達到10000 原因是:volatile修飾的變量并不保證對它的操作(自增)具有原子性。(對于自增操作,可以使用JAVA的原子類AutoicInteger類保證原子自增) 比如,假設(shè) i 自增到 5,線程A從主內(nèi)存中讀取i,值為5,將它存儲到自己的線程空間中,執(zhí)行加1操作,值為6。此時,CPU切換到線程B執(zhí)行,從主從內(nèi)存中讀取變量i的值。由于線程A還沒有來得及將加1后的結(jié)果寫回到主內(nèi)存,線程B就已經(jīng)從主內(nèi)存中讀取了i,因此,線程B讀到的變量 i 值還是5 相當(dāng)于線程B讀取的是已經(jīng)過時的數(shù)據(jù)了,從而導(dǎo)致線程不安全性。這種情形在《Effective JAVA》中稱之為“安全性失敗” 綜上,僅靠volatile不能保證線程的安全性。(原子性)
此外,volatile關(guān)鍵字修飾的變量不會被指令重排序優(yōu)化。這里以《深入理解JAVA虛擬機》中一個例子來說明下自己的理解: 線程A執(zhí)行的操作如下: Map configOptions ;
char[] configText;
volatile boolean initialized = false;
//線程A首先從文件中讀取配置信息,調(diào)用process...處理配置信息,處理完成了將initialized 設(shè)置為true
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfig(configText, configOptions);//負(fù)責(zé)將配置信息configOptions 成功初始化
initialized = true;
線程B等待線程A把配置信息初始化成功后,使用配置信息去干活.....線程B執(zhí)行的操作如下:
如果initialized變量不用 volatile 修飾,在線程A執(zhí)行的代碼中就有可能指令重排序。 即:線程A執(zhí)行的代碼中的最后一行:initialized = true 重排序到了 processConfig方法調(diào)用的前面執(zhí)行了,這就意味著:配置信息還未成功初始化,但是initialized變量已經(jīng)被設(shè)置成true了。那么就導(dǎo)致 線程B的while循環(huán)“提前”跳出,拿著一個還未成功初始化的配置信息去干活(doSomethingWithConfig方法)。。。。 因此,initialized 變量就必須得用 volatile修飾。這樣,就不會發(fā)生指令重排序,也即:只有當(dāng)配置信息被線程A成功初始化之后,initialized 變量才會初始化為true。綜上,volatile 修飾的變量會禁止指令重排序(有序性)
三,volatile 與 synchronized 的比較 volatile主要用在多個線程感知實例變量被更改了場合,從而使得各個線程獲得最新的值。它強制線程每次從主內(nèi)存中講到變量,而不是從線程的私有內(nèi)存中讀取變量,從而保證了數(shù)據(jù)的可見性。 關(guān)于synchronized,可參考:JAVA多線程之Synchronized關(guān)鍵字--對象鎖的特點 比較: ①volatile輕量級,只能修飾變量。synchronized重量級,還可修飾方法 ②volatile只能保證數(shù)據(jù)的可見性,不能用來同步,因為多個線程并發(fā)訪問volatile修飾的變量不會阻塞。 synchronized不僅保證可見性,而且還保證原子性,因為,只有獲得了鎖的線程才能進入臨界區(qū),從而保證臨界區(qū)中的所有語句都全部執(zhí)行。多個線程爭搶synchronized鎖對象時,會出現(xiàn)阻塞。
四,線程安全性 線程安全性包括兩個方面,①可見性。②原子性。 從上面自增的例子中可以看出:僅僅使用volatile并不能保證線程安全性。而synchronized則可實現(xiàn)線程的安全性。
|
|