java多線程中 volatile與synchronized的區別-阿里面試 java中volatile關鍵字的含義

volatile 與 synchronized 的比較(阿里面試官問的問題)  html

①volatile輕量級,只能修飾變量。synchronized重量級,還可修飾方法 java

②volatile只能保證數據的可見性,不能用來同步,由於多個線程併發訪問volatile修飾的變量不會阻塞。 面試

synchronized不只保證可見性,並且還保證原子性,由於,只有得到了鎖的線程才能進入臨界區,從而保證臨界區中的全部語句都所有執行。多個線程爭搶synchronized鎖對象時,會出現阻塞。 緩存

volatile本質是在告訴jvm當前變量在寄存器中的值是不肯定的,須要從主存中讀取,synchronized則是鎖定當前變量,只有當前線程能夠訪問該變量,其餘線程被阻塞住.
volatile僅能使用在變量級別,synchronized則可使用在變量,方法.
volatile僅能實現變量的修改可見性,但不具有原子特性,而synchronized則能夠保證變量的修改可見性和原子性.
volatile不會形成線程的阻塞,而synchronized可能會形成線程的阻塞.
volatile標記的變量不會被編譯器優化,而synchronized標記的變量能夠被編譯器優化. 安全

二,線程安全性 多線程

線程安全性包括兩個方面,①可見性。②原子性。 併發

僅僅使用volatile並不能保證線程安全性。而synchronized則可實現線程的安全性。 jvm

三,volatile關鍵字的可見性 ide

要想理解volatile關鍵字,得先了解下JAVA的內存模型,Java內存模型的抽象示意圖以下: post

 

從圖中能夠看出: 

①每一個線程都有一個本身的本地內存空間--線程棧空間 線程執行時,先把變量從主內存讀取到線程本身的本地內存空間,而後再對該變量進行操做 

②對該變量操做完後,在某個時間再把變量刷新回主內存

在Java中,爲了保證多線程讀寫數據時保證數據的一致性,能夠採用兩種方式: 

(a)如用synchronized關鍵字,或者使用鎖對象.

(b)使用volatile關鍵字,用一句話歸納volatile,它可以使變量在值發生改變時能儘快地讓其餘線程知道. 

synchronized 

全部加上synchronized 和 塊語句,在多線程訪問的時候,同一時刻只能有一個線程可以用synchronized 修飾的方法 或者 代碼塊。 

volatile

首先咱們要先意識到有這樣的現象,編譯器爲了加快程序運行的速度,對一些變量的寫操做會先在寄存器或者是CPU緩存上進行,最後才寫入內存.

而在這個過程,變量的新值對其餘線程是不可見的.而volatile的做用就是使它修飾的變量的讀寫操做都必須在內存中進行! 

用volatile修飾的變量,線程在每次使用變量的時候,都會讀取變量修改後的最的值。volatile很容易被誤用,用來進行原子性操做。

對於volatile修飾的變量,JVM虛擬機只保證從主內存加載到線程工做內存的值是最新的。 

所以,就存在內存可見性問題,看一個示例程序: 

 1 public class RunThread extends Thread {
 2      private boolean isRunning = true;           
 3                                                  
 4      public boolean isRunning() {                
 5          return isRunning;                       
 6      }                                           
 7                                                  
 8      public void setRunning(boolean isRunning) { 
 9          this.isRunning = isRunning;             
10      }                                           
11                                                  
12      @Override                                   
13      public void run() {                         
14          System.out.println("進入到run方法中了");
15          while (isRunning == true) {             
16          }                                       
17          System.out.println("線程執行完成了");   
18      }                                           
19  }                                               
20                                                  
21  public class Run {                              
22      public static void main(String[] args) {    
23          try {                                   
24              RunThread thread = new RunThread(); 
25              thread.start();                     
26              Thread.sleep(1000);                 
27              thread.setRunning(false);           
28          } catch (InterruptedException e) {      
29              e.printStackTrace();                
30          }                                       
31      }                                           
32  }   

Run.java 第28行,main線程 將啓動的線程RunThread中的共享變量設置爲false,從而想讓RunThread.java 第14行中的while循環結束。 

若是,咱們使用JVM -server參數執行該程序時,RunThread線程並不會終止!從而出現了死循環!! 

緣由分析: 

如今有兩個線程,一個是main線程,另外一個是RunThread。它們都試圖修改 第三行的 isRunning變量。按照JVM內存模型,main線程將isRunning讀取到本地線程內存空間,修改後,再刷新回主內存。 

而在JVM 設置成 -server模式運行程序時,線程會一直在私有堆棧中讀取isRunning變量。所以,RunThread線程沒法讀到main線程改變的isRunning變量 

從而出現了死循環,致使RunThread沒法終止 

解決方法,在第三行代碼處用 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 修飾的變量會禁止指令重排序(有序性)  

在 java 垃圾回收整理一文中,描述了jvm運行時刻內存的分配。其中有一個內存區域是jvm虛擬機棧,每個線程運行時都有一個線程棧,

線程棧保存了線程運行時候變量值信息。當線程訪問某一個對象時候值的時候,首先經過對象的引用找到對應在堆內存的變量的值,而後把堆內存

變量的具體值load到線程本地內存中,創建一個變量副本,以後線程就再也不和對象在堆內存變量值有任何關係,而是直接修改副本變量的值,

在修改完以後的某一個時刻(線程退出以前),自動把線程變量副本的值回寫到對象在堆中變量。這樣在堆中的對象的值就產生變化了。下面一幅圖

描述這寫交互 

java volatile1

  

read and load 從主存複製變量到當前工做內存
use and assign  執行代碼,改變共享變量值 
store and write 用工做內存數據刷新主存相關內容

其中use and assign 能夠屢次出現

可是這一些操做並非原子性,也就是 在read load以後,若是主內存count變量發生修改以後,線程工做內存中的值因爲已經加載,不會產生對應的變化,因此計算出來的結果會和預期不同

對於volatile修飾的變量,jvm虛擬機只是保證從主內存加載到線程工做內存的值是最新的

例如假如線程1,線程2 在進行read,load 操做中,發現主內存中count的值都是5,那麼都會加載這個最新的值

在線程1堆count進行修改以後,會write到主內存中,主內存中的count變量就會變爲6

線程2因爲已經進行read,load操做,在進行運算以後,也會更新主內存count的變量值爲6

致使兩個線程及時用volatile關鍵字修改以後,仍是會存在併發的狀況。 

下面看一個例子,咱們實現一個計數器,每次線程啓動的時候,會調用計數器inc方法,對計數器進行加一 

執行環境——jdk版本:jdk1.6.0_31 ,內存 :3G   cpu:x86 2.4G

public class Counter {
     public static int count = 0; 
    public static void inc() { 
        //這裏延遲1毫秒,使得結果明顯
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
        } 
        count++;
    } 
    public static void main(String[] args) { 
        //同時啓動1000個線程,去進行i++計算,看看實際結果 
        for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Counter.inc();
                }
            }).start();
        } 
        //這裏每次運行的值都有可能不一樣,可能爲1000
        System.out.println("運行結果:Counter.count=" + Counter.count);
    }
}

輸出爲:

運行結果:Counter.count=995

實際運算結果每次可能都不同,本機的結果爲:運行結果:Counter.count=995,能夠看出,在多線程的環境下,Counter.count並無指望結果是100 

不少人覺得,這個是多線程併發問題,只須要在變量count以前加上volatile就能夠避免這個問題,那咱們在修改代碼看看,看看結果是否是符合咱們的指望 

public class Counter { 
    public volatile static int count = 0; 
    public static void inc() { 
        //這裏延遲1毫秒,使得結果明顯
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
        } 
        count++;    } 
    public static void main(String[] args) { 
        //同時啓動1000個線程,去進行i++計算,看看實際結果
 
        for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Counter.inc();
                }
            }).start();
        }
 
        //這裏每次運行的值都有可能不一樣,可能爲1000
        System.out.println("運行結果:Counter.count=" + Counter.count);
    }
}

運行結果:

運行結果:Counter.count=992

 運行結果仍是沒有咱們指望的1000

參考:java中volatile關鍵字的含義 

相關文章
相關標籤/搜索