JAVA 多線程之volatile的介紹

volatile的介紹

volatile的主要做用是:提示編譯器該對象的值有可能在編譯器未監測的狀況下被改變。   java

volatile相似於你們所熟知的const也是一個類型修飾符。volatile是給編譯器的指示來講明對它所修飾的對象不該該執行優化。volatile的做用就是用來進行多線程編程。在單線程中那就是隻能起到限制編譯器優化的做用。因此單線程的童鞋們就不用浪費精力看下面的了。編程

 

volatile讓變量每次在使用的時候,都從主存中取。而不是從各個線程的「工做內存」。安全

volatile具備synchronized關鍵字的「可見性」,可是沒有synchronized關鍵字的「併發正確性」,也就是說不保證線程執行的有序性。多線程

也就是說,volatile變量對於每次使用,線程都能獲得當前volatile變量的最新值。可是volatile變量並不保證併發的正確性。併發

=========================分割線1================================= 框架

在Java內存模型中,有main memory,每一個線程也有本身的memory (例如寄存器)。爲了性能,一個線程會在本身的memory中保持要訪問的變量的副本。這樣就會出現同一個變量在某個瞬間,在一個線程的memory中的值可能與另一個線程memory中的值,或者main memory中的值不一致的狀況。 

一個變量聲明爲volatile,就意味着這個變量是隨時會被其餘線程修改的,所以不能將它cache在線程memory中。ide

沒有volatile的結果

      若是沒有volatile,你將沒法在多線程中並行使用到基本變量。下面舉一個我開發項目的實例(這個實例採用的是C#語言但不妨礙咱們討論C++)。在學校的一個.Net項目的開發中,我曾經在多線程監控中用到過一個基本變量Int32型的,我用它來控制多線程中監控的一個條件。考慮到基本變量是編譯器自帶的並且沒法用lock鎖上,我想固然的覺得是原子操做不會有多線程的問題,可實際運行後發現程序的運行有時正常有時異常,改成用Dictionary對象處理並加鎖之後才完全正常。如今想來應該是多線程同時操做該變量了,具體的將在下面說清。函數

volatile的做用

      若是一個基本變量被volatile修飾,編譯器將不會把它保存到寄存器中,而是每一次都去訪問內存中實際保存該變量的位置上。這一點就避免了沒有volatile修飾的變量在多線程的讀寫中所產生的因爲編譯器優化所致使的災難性問題。因此多線程中必需要共享的基本變量必定要加上volatile修飾符。固然了,volatile還能讓你在編譯時期捕捉到非線程安全的代碼。我在下面還會介紹一位大牛使用智能指針來順序化共享區代碼的方法,在此對其表示感謝。工具

      泛型編程中曾經說過編寫異常安全的代碼是很困難的,但是相比起多線程編程的困難來講這就過小兒科了。多線程編程中你須要證實它正確,須要去反覆地枯燥地調試並修復,固然了,資源競爭也是必須注意的,最可恨的是,有時候編譯器也會給你點顏色看看。。。性能

class Student { public: void Wait() //在北航排隊等吃飯實在是很痛苦的事情。。。 { while (!flag) { Sleep(1000); // sleeps for 1000 milliseconds } } void eat() { flag = true; } ... private: bool flag; };

      好吧,多線程中你就等着吃飯吧,可在這個地方估計你是永遠等不到了,由於flag被編譯器放到寄存器中去了,哪怕在你前面的那位童鞋告訴你flag=true了,可你就好像瞎了眼看不到這些了。這麼詭異的狀況的發生時由於你所用到的判斷值是以前保存到寄存器中的,這樣原來的地址上的flag值更改了你也沒有獲取。該怎麼辦呢?對了,改爲volatile就解決了。

      volatile對基本類型和對用戶自定義類型的使用與const有區別,好比你能夠把基本類型的non-volatile賦值給volatile,但不能把用戶自定義類型的non-volatile賦值給volatile,而const都是能夠的。還有一個區別就是編譯器自動合成的複製控制不適用於volatile對象,由於合成的複製控制成員接收const形參,而這些形參又是對類類型的const引用,可是不能將volatile對象傳遞給普通引用或const引用。

如何在多線程中使用好volatile

      在多線程中,咱們能夠利用鎖的機制來保護好資源臨界區。在臨界區的外面操做共享變量則須要volatile,在臨界區的裏面則non-volatile了。咱們須要一個工具類LockingPtr來保存mutex的採集和volatile的利用const_cast的轉換(經過const_cast來進行volatile的轉換)。

      首先咱們聲明一個LockingPtr中要用到的Mutex類的框架:

class Mutex { public: void Acquire(); void Release(); ... };

      接着聲明最重要的LockingPtr模板類:

template <typename T> class LockingPtr { public: // Constructors/destructors LockingPtr(volatile T& obj, Mutex& mtx) : pObj_(const_cast<T*>(&obj)), pMtx_(&mtx) { mtx.Lock(); } ~LockingPtr() { pMtx_->Unlock(); } // Pointer behavior T& operator*() { return *pObj_; } T* operator->() { return pObj_; } private: T* pObj_; Mutex* pMtx_; LockingPtr(const LockingPtr&); LockingPtr& operator=(const LockingPtr&); };

      儘管這個類看起來簡單,可是它在編寫爭取的多線程程序中很是的有用。你能夠經過對它的使用來使得對多線程中共享的對象的操做就好像對volatile修飾的基本變量同樣簡單並且從不會使用到const_cast。下面來給一個例子:

       假設有兩個線程共享一個vector<char>對象:

class SyncBuf { public: void Thread1(); void Thread2(); private: typedef vector<char> BufT; volatile BufT buffer_; Mutex mtx_; // controls access to buffer_ };

      在函數Thread1中,你經過lockingPtr<BufT>來控制訪問buffer_成員變量:

void SyncBuf::Thread1() { LockingPtr<BufT> lpBuf(buffer_, mtx_); BufT::iterator i = lpBuf->begin(); for (; i != lpBuf->end(); ++i) { ... use *i ... } }

      這個代碼很容易編寫和理解。只要你須要用到buffer_你必須建立一個lockingPtr<BufT>指針來指向它,而且一旦你這麼作了,你就得到了容器vector的整個接口。並且你一旦犯錯,編譯器就會指出來:

void SyncBuf::Thread2() { // Error! Cannot access 'begin' for a volatile object BufT::iterator i = buffer_.begin(); // Error! Cannot access 'end' for a volatile object for (; i != lpBuf->end(); ++i) { ... use *i ... } }

      這樣的話你就只有經過const_cast或LockingPtr來訪問成員函數和變量了。這兩個方法的不一樣之處在於後者提供了順序的方法來實現而前者是經過轉換爲volatile來實現。LockingPtr是至關好理解的,若是你須要調用一個函數,你就建立一個未命名的暫時的LockingPtr對象並直接使用:

unsigned int SyncBuf::Size() { return LockingPtr<BufT>(buffer_, mtx_)->size(); }

LockingPtr在基本類型中的使用

      在上面咱們分別介紹了使用volatile來保護對象的意外訪問和使用LockingPtr來提供簡單高效的多線程代碼。如今來討論比較常見的多線程處理共享基本類型的一種狀況:

class Counter { public: ... void Increment() { ++ctr_; } void Decrement() { —-ctr_; } private: int ctr_; };

      這個時候可能你們都能看出來問題所在了。1.ctr_須要是volatile型。2.即使是++ctr_或--ctr_,這在處理中還是須要三個原子操做的(Read-Modify-Write)。基於上述兩點,這個類在多線程中會有問題。如今咱們就來利用LockingPtr來解決:

class Counter { public: ... void Increment() { ++*LockingPtr<int>(ctr_, mtx_); } void Decrement() { —?*LockingPtr<int>(ctr_, mtx_); } private: volatile int ctr_; Mutex mtx_; };

 

volatile成員函數

      關於類的話,首先若是類是volatile則裏面的成員都是volatile的。其次要將成員函數聲明爲volatile則同const同樣在函數最後聲明便可。當你設計一個類的時候,你聲明的那些volatile成員函數是線程安全的,因此那些隨時可能被調用的函數應該聲明爲volatile。考慮到volatile等於線程安全代碼和非臨界區;non-volatile等於單線程場景和在臨界區之中。咱們能夠利用這個作一個函數的volatile的重載來在線程安全和速度優先中作一個取捨。具體的實現此處就略去了。

 

在當前的Java內存模型下,線程能夠把變量保存在本地內存(好比機器的寄存器)中,而不是直接在主存中進行讀寫。這就可能形成一個線程在主存中修改了一個變量的值,而另一個線程還繼續使用它在寄存器中的變量值的拷貝,形成數據的不一致。 



要解決這個問題,只須要像在本程序中的這樣,把該變量聲明爲volatile(不穩定的)便可,這就指示JVM,這個變量是不穩定的,每次使用它都到主存中進行讀取。通常說來,多任務環境下各任務間共享的標誌都應該加volatile修飾。 


Volatile修飾的成員變量在每次被線程訪問時,都強迫從共享內存中重讀該成員變量的值。並且,當成員變量發生變化時,強迫線程將變化值回寫到共享內存。這樣在任什麼時候刻,兩個不一樣的線程老是看到某個成員變量的同一個值。 

 

用volatile和不用volatile的區別,運行一下,就知道了。

不用volatile:

 
  1. package com.keyword;  
  2.   
  3. public class TestWithoutVolatile {  
  4.     private static boolean bChanged;  
  5.   
  6.     public static void main(String[] args) throws InterruptedException {  
  7.         new Thread() {  
  8.   
  9.             @Override  
  10.             public void run() {  
  11.                 for (;;) {  
  12.                     if (bChanged == !bChanged) {  
  13.                         System.out.println("!=");  
  14.                         System.exit(0);  
  15.                     }  
  16.                 }  
  17.             }  
  18.         }.start();  
  19.         Thread.sleep(1);  
  20.         new Thread() {  
  21.   
  22.             @Override  
  23.             public void run() {  
  24.                 for (;;) {  
  25.                     bChanged = !bChanged;  
  26.                 }  
  27.             }  
  28.         }.start();  
  29.     }  
  30.   
  31. }  


運行後,程序進入死循環了,一直在運行。

 

 

用volatile:

 package com.keyword;  

  1.   
  2. public class TestWithVolatile {  
  3.     private static volatile boolean bChanged;  
  4.   
  5.     public static void main(String[] args) throws InterruptedException {  
  6.         new Thread() {  
  7.   
  8.             @Override  
  9.             public void run() {  
  10.                 for (;;) {  
  11.                     if (bChanged == !bChanged) {  
  12.                         System.out.println("!=");  
  13.                         System.exit(0);  
  14.                     }  
  15.                 }  
  16.             }  
  17.         }.start();  
  18.         Thread.sleep(1);  
  19.         new Thread() {  
  20.   
  21.             @Override  
  22.             public void run() {  
  23.                 for (;;) {  
  24.                     bChanged = !bChanged;  
  25.                 }  
  26.             }  
  27.         }.start();  
  28.     }  
  29.   
  30. }  


程序輸出!=,而後立刻退出。

 

可是,不少狀況下,用不用volatile,感受不出什麼區別,何時要用volatile呢?看看JDK裏使用volatile的類。

好比java.util.regex.Pattern裏的變量:

 
  1. private transient volatile boolean compiled = false;  


還有,java.lang.System的變量:

  1. private static volatile Console cons = null;  


通常就是初始化的時候,須要用到volatile。

 

java.util.Scanner裏的變量,如:

  1. private static volatile Pattern boolPattern;  
  2. private static volatile Pattern separatorPattern;  
  3. private static volatile Pattern linePattern;  


初始化boolPattern的代碼:

 
  1. private static Pattern boolPattern() {  
  2.         Pattern bp = boolPattern;  
  3.         if (bp == null)  
  4.             boolPattern = bp = Pattern.compile(BOOLEAN_PATTERN,  
  5.                                           Pattern.CASE_INSENSITIVE);  
  6.         return bp;  
  7. }  

 

 

上面的狀況,可使用synchronized來對boolPattern加鎖,可是synchronized開銷比volatile大,volatile可以勝任上面的工做。

 

volatile不保證原子操做,因此,很容易讀到髒數據。

 

使用建議:在兩個或者更多的線程訪問的成員變量上使用volatile。當要訪問的變量已在synchronized代碼塊中,或者爲常量時,沒必要使用。

總結

      在編寫多線程程序中使用volatile的關鍵四點:

      1.將全部的共享對象聲明爲volatile;

      2.不要將volatile直接做用於基本類型;

      3.當定義了共享類的時候,用volatile成員函數來保證線程安全;

      4.多多理解和使用volatile和LockingPtr!(強烈建議)

 

轉自:http://blog.csdn.net/jingxuewang110/article/details/6759044

相關文章
相關標籤/搜索