1. 程式人生 > >Java併發程式設計——volatile

Java併發程式設計——volatile

volatile可以看成是輕量級的低配版的Synchronized,他主要是作用於共享變數,保證共享變數的可見性。確保共享變數在主記憶體中一致地準確的更新通知到各個執行緒,這是Volatile的可見性,同時由於它是低配版的Synchronized,所以他也沒有了Synchronized的一些功能,比如原子性。

Java記憶體模型

在理解有關Java併發程式設計的時候,我們是非常有必要的先了解一下Java記憶體模型的。正如圖上所示,Java記憶體模型規定了所有的變數都儲存在主記憶體中,執行緒之間的工作並不是直接去讀取操作主記憶體的,而是每個工作執行緒首先會在主記憶體中拷貝這些共享變數下來執行緒對變數的操作都是自己的工作執行緒中完成的,對於不同執行緒之間是具有不具備可見性的(各做各的),執行緒間變數值的傳遞均需要通過主記憶體來完成。Volatile正是為了解決以上的問題而存在的。

工作執行緒操作變數與主記憶體的互動

线ç¨åå­ä¸ä¸»åå­äº¤äº

  • read:從主記憶體中讀取變數
  • load:複製變數到工作執行緒本地記憶體作為副本
  • use:運算副本
  • assign:給副本賦值
  • store:副本寫入工作執行緒的記憶體中
  • write:執行緒記憶體中的共享副本重新整理主記憶體中的共享變數

 

可見性

可見性是指執行緒之間變數的可見性,及時得到變數狀態變化的通知,一個執行緒修改了變數的狀態另一個就及時的知道變數的最新狀態。舉個例子:A、B執行緒在主記憶體中拷貝同一個Volatile修飾變數,A執行緒把這個變數的狀態由false改為了true,緊跟著B執行緒就會收到通知他剛剛拷貝的變數已經過期失效,B執行緒就會更新這個變數,得到最新的狀態true,而不再是過期失效的狀態。

Volatile修飾的變數不允許執行緒內部快取和重排序,也就是說直接操作主記憶體,這樣就對其他執行緒可見了。但是,但是,但是,Volatile修飾的變數只能保證變數的可見性,而不能保證變數的原子性,這樣就導致一些非原子性的操作仍然存線上程安全的問題。

普通的共享變數在進行操作之後,寫入主記憶體的時機是不確定的,該執行緒可能在操作完變數並且還沒寫入主記憶體的時候就去幹別的事情了,這樣就導致在其他執行緒獲取這個變數的時候並不是最新的值,無法保證可見性,真是這樣的執行緒安全問題也就導致了程式執行結果並不是我們所期望的結果。

Volatile並不能保證原子性,而他的高配版——Synchronized完全應付了這些問題。Synchronized既能保證可見性又能保證原子性。Synchronized在工作時只能有一個執行緒獲取鎖執行同步程式碼,並在釋放鎖的時候把變數寫入主記憶體中。

public class MyThread extends Thread {
    public boolean exit = false; 
        public void run() { 
        while (!exit){
            //do something
        }
    } 
}

上面的程式碼使用退出標誌終止執行緒關閉的程式碼。看上去似乎是沒有問題,只要其他執行緒吧exit複製為true就能夠終止執行緒。但是這樣寫仍然會存在風險,有可能不是我們所期望的效果。上面說過,工作執行緒會各自在主執行緒中拷貝變數,然後自顧自的工作。以例項來說,A、B執行緒會在主執行緒中各自拷貝exit變數到自己的工作記憶體中,當B需要終止A執行緒的時候,B便會修改自己工作記憶體中的exit變數,但是由於不確定性B在修改本地exit變數的時候可能還沒把修改後的變數exit寫入主記憶體中就去了幹別的事情了,導致A執行緒沒有終止。

給exit變數增加Volatile修飾後,B執行緒把本地變數exit賦值為true的時候,Volatile會強制把最新值寫到主記憶體中並且會通知A執行緒告知其本地exit變數已過期失效,立即到主記憶體中更新exit變數,這樣子便會使A執行緒的exit變數及時更新。也體現了Volatile線上程之間的可見性。

原子性

從Volatile可見性的問題中我們帶出了原子性這一名詞。原子性是指:一個操作或者多個操作(可以把它看成事務)要麼全執行而且不會被中斷,要麼全不執行。在Java中,對基本資料型別的變數的讀取和賦值操作是就原子性操作。原子就是不能再細分的意思。

舉個例子:int a = 8; 把8賦值給a這個操作已經不能再細分或分割了,那麼類似於這種操作就稱之為原子操作。

再舉個例子:i++; 這個操作就可以分解為 i = i + 1 ,那麼類似於這從操作就稱之為非原子操作

非原子操作帶來的是執行緒安全問題,使用同步技術Synchronized來把這堆操作變成一個原子操作。

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(){
                @Override                
                public void run() {                    
                    for(int j=0;j<1000;j++)                        
                        test.increase();                
                    };            
            }.start();        
        }         

        while(Thread.activeCount()>1){
            System.out.println(test.inc);   
        }  //保證前面的執行緒都執行完 
           
        Thread.yield();         
    }
}

上面程式碼,我們直觀的認為新建了10個執行緒,每個執行緒都對inc變數自增1,那麼10個執行緒最後輸出的結果自然是1000*10=10000,這是我們所期望的。但是通過輸出我們發現結果並不是我們所想要的。

前面提到過,Volatile只能保證變數的可見性,而不能保證原子性。

還是按例項來說,A、B執行緒建立後各自把inc拷貝到自己的本地記憶體中。此時A、B都在自己本地執行緒中對inc++自增,然後A、B執行緒把運算後的結果寫入主記憶體中。這樣,儘管A、B執行緒都進行了自增的運算,我們的期望是等於3,儘管進行了兩次自增,但是此時主記憶體中的inc變數只是2。

例子也體現了Volatile並不能保證非原子操作,仍然會存線上程安全問題。

解決方案:可以通過synchronized或lock,進行加鎖,來保證操作的原子性。也可以通過AtomicInteger。(不在本文範圍)

有序性

有序性就是程式執行的順序按照程式碼的先後順序執行。Java記憶體模型具備一些先天的“有序性”,即不需要通過任何手段就能夠得到保證的有序性,這個通常也稱為 happens-before 原則。如果兩個操作的執行次序無法從happens-before原則推匯出來,那麼它們就不能保證它們的有序性,虛擬機器可以隨意地對它們進行重排序。

處理器為了提高程式執行效率,可能會對輸入程式碼進行優化,它不保證程式中各個語句的執行先後順序同程式碼中的順序一致,但是它會保證程式最終執行結果和程式碼順序執行的結果是一致的。處理器的重排序不會影響單個執行緒,但是面臨併發程式設計的時候就不能保證正確性了。

volatile關鍵字可以保證一定的“有序性”。synchronized既保證有序性同樣保證原子性。

Volatile原理

在對聲明瞭volatile變數進行寫操作時,JVM會向處理器傳送一條Lock字首的指令,將這個變數所在快取行的資料寫會到系統記憶體。 這一步確保瞭如果有其他執行緒對聲明瞭volatile變數進行修改,則立即更新主記憶體中資料。

為了保證各個處理器的工作執行緒一致,每個處理會通過嗅探在總線上傳播的資料來檢查自己的快取是否過期,當處理器發現自己快取行對應的記憶體地址被修改了,就會將當前處理器的快取行設定成無效狀態,當處理器要對這個資料進行修改操作時,會強制重新從系統記憶體把資料讀到處理器快取裡。

小結

synchronized​是防止多個執行緒同時執行一段程式碼,這樣同步就會影響程式執行效率,而volatile在某些情況下效能要優於synchronized。Volatile只能保證變數的可見性,並不能保證變數的原子性。對於由於非原子操作而產生的執行緒安全問題,還是請使用synchronized。

最後使用Volatile的必備兩個條件:

  • 對變數的操作不依賴於當前值,也就是原子操作
  • 該變數沒有包含在具有其他變數的不變式中

That's all Thank you~

                                                                                      更多好文

                                                                              請掃描下面二維碼

                                                                                     歡迎關注~