一,volatile關鍵字的可見性html
要想理解volatile關鍵字,得先了解下JAVA的內存模型,Java內存模型的抽象示意圖以下:java
從圖中能夠看出:安全
①每一個線程都有一個本身的本地內存空間--線程棧空間???線程執行時,先把變量從主內存讀取到線程本身的本地內存空間,而後再對該變量進行操做多線程
②對該變量操做完後,在某個時間再把變量刷新回主內存併發
關於JAVA內存模型,更詳細的可參考: 深刻理解Java內存模型(一)——基礎ide
所以,就存在內存可見性問題,看一個示例程序:(摘自書上)post
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("線程執行完成了"); 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中的共享變量設置爲false,從而想讓RunThread.java 第14行中的while循環結束。優化
若是,咱們使用JVM -server參數執行該程序時,RunThread線程並不會終止!從而出現了死循環!!this
緣由分析:spa
如今有兩個線程,一個是main線程,另外一個是RunThread。它們都試圖修改 第三行的 isRunning變量。按照JVM內存模型,main線程將isRunning讀取到本地線程內存空間,修改後,再刷新回主內存。
而在JVM 設置成 -server模式運行程序時,線程會一直在私有堆棧中讀取isRunning變量。所以,RunThread線程沒法讀到main線程改變的isRunning變量
從而出現了死循環,致使RunThread沒法終止。這種情形,在《Effective JAVA》中,將之稱爲「活性失敗」
解決方法,在第三行代碼處用 volatile 關鍵字修飾便可。這裏,它強制線程從主內存中取 volatile修飾的變量。
volatile private boolean isRunning = true;
擴展一下,當多個線程之間須要根據某個條件肯定 哪一個線程能夠執行時,要確保這個條件在 線程 之間是可見的。所以,能夠用volatile修飾。
綜上,volatile關鍵字的做用是:使變量在多個線程間可見(可見性)
二,volatile關鍵字的非原子性
所謂原子性,就是某系列的操做步驟要麼所有執行,要麼都不執行。
好比,變量的自增操做 i++,分三個步驟:
①從內存中讀取出變量 i 的值
②將 i 的值加1
③將 加1 後的值寫回內存
這說明 i++ 並非一個原子操做。由於,它分紅了三步,有可能當某個線程執行到了第②時被中斷了,那麼就意味着只執行了其中的兩個步驟,沒有所有執行。
關於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循環中建立了100個線程,第25行將這100個線程啓動去執行 addCount(),每一個線程執行100次加1
指望的正確的結果應該是 100*100=10000,可是,實際上count並無達到10000
緣由是:volatile修飾的變量並不保證對它的操做(自增)具備原子性。(對於自增操做,可使用JAVA的原子類AutoicInteger類保證原子自增)
好比,假設 i 自增到 5,線程A從主內存中讀取i,值爲5,將它存儲到本身的線程空間中,執行加1操做,值爲6。此時,CPU切換到線程B執行,從主從內存中讀取變量i的值。因爲線程A尚未來得及將加1後的結果寫回到主內存,線程B就已經從主內存中讀取了i,所以,線程B讀到的變量 i 值仍是5
至關於線程B讀取的是已通過時的數據了,從而致使線程不安全性。這種情形在《Effective JAVA》中稱之爲「安全性失敗」
綜上,僅靠volatile不能保證線程的安全性。(原子性)
此外,volatile關鍵字修飾的變量不會被指令重排序優化。這裏以《深刻理解JAVA虛擬機》中一個例子來講明下本身的理解:
線程A執行的操做以下:
Map configOptions ; char[] configText; volatile boolean initialized = false; //線程A首先從文件中讀取配置信息,調用process...處理配置信息,處理完成了將initialized 設置爲true configOptions = new HashMap(); configText = readConfigFile(fileName); processConfig(configText, configOptions);//負責將配置信息configOptions 成功初始化 initialized = true;
線程B等待線程A把配置信息初始化成功後,使用配置信息去幹活.....線程B執行的操做以下:
while(!initialized) { sleep(); } //使用配置信息幹活 doSomethingWithConfig();
若是initialized變量不用 volatile 修飾,在線程A執行的代碼中就有可能指令重排序。
即:線程A執行的代碼中的最後一行:initialized = true 重排序到了 processConfig方法調用的前面執行了,這就意味着:配置信息還未成功初始化,可是initialized變量已經被設置成true了。那麼就致使 線程B的while循環「提早」跳出,拿着一個還未成功初始化的配置信息去幹活(doSomethingWithConfig方法)。。。。
所以,initialized 變量就必須得用 volatile修飾。這樣,就不會發生指令重排序,也即:只有當配置信息被線程A成功初始化以後,initialized 變量纔會初始化爲true。綜上,volatile 修飾的變量會禁止指令重排序(有序性)
三,volatile 與 synchronized 的比較
volatile主要用在多個線程感知實例變量被更改了場合,從而使得各個線程得到最新的值。它強制線程每次從主內存中講到變量,而不是從線程的私有內存中讀取變量,從而保證了數據的可見性。
關於synchronized,可參考:JAVA多線程之Synchronized關鍵字--對象鎖的特色
比較:
①volatile輕量級,只能修飾變量。synchronized重量級,還可修飾方法
②volatile只能保證數據的可見性,不能用來同步,由於多個線程併發訪問volatile修飾的變量不會阻塞。
synchronized不只保證可見性,並且還保證原子性,由於,只有得到了鎖的線程才能進入臨界區,從而保證臨界區中的全部語句都所有執行。多個線程爭搶synchronized鎖對象時,會出現阻塞。
四,線程安全性
線程安全性包括兩個方面,①可見性。②原子性。
從上面自增的例子中能夠看出:僅僅使用volatile並不能保證線程安全性。而synchronized則可實現線程的安全性。