線程安全與併發編程探究(七)--volatile java內存模型及線程知識小結

一、volatile與java內存模型

當一個變量定義爲volatile之後,可以保證此共享變量對所有其他線程的可見性,即一條線程修改了該變量的值,則新值對於其他線程來說都是可以立即得知的立即可見的。對volatile變量的寫操作都能立刻反映到其他線程之中,即volatile變量在各個線程之中是一致的。注意:java的內存模型JMM分爲主內存和工作內存。所有線程間共享的變量都存儲在主內存Main Memory中,每條線程都有自己的工作內存Working Memory,線程的工作內存中保存了被該線程使用到的變量的主內存的副本拷貝,線程對變量的所有操作(讀取賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量。不同線程間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要通過主內存來完成。(特別注意:根據java虛擬機規範的規定,volatile變量依然有工作內存的拷貝,由於執行引擎在每次使用該volatile變量前都要先刷新,故看不到不一致的情況,所以看起來如同直接在主內存中讀寫訪問一般)。

使用volatile變量的第二個語義就是禁止指令重排序優化。普通變量僅僅會保證在該方法的執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證該變量賦值操作的順序與程序代碼中的執行順序一致。因爲在一個線程的方法執行過程中無法感知到這點,這也是java內存模型中描述的所謂的“線程內表現爲串行的語義”(Within-Thread As-If-Serial Semantics)

Java內存模型的三個特徵是原子性、可見性、有序性。

l  原子性(Atomicity):synchronized塊之間的操作具備原子性。

l  可見性(Visibility):指一個線程修改了某共享變量的值,其他線程能夠立即得知這個修改。JAVA內存模型是通過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存作爲傳遞媒介的方法來實現可見性的,無論是普通變量還是volatile變量都是如此。區別是:volatile的特殊規則保證了新值能立即同步到主內存,以及每次使用前立即從主內存刷新。因爲,可以說保證了多線程操作時變量的可見性。而普通變量不能保證這一點。另外,synchronized和final這兩個關鍵字也能實現可見性。

l  有序性(Ordering):java內存模型的有序性是指:如果在本線程內觀察,所有的操作都是有序的,如果在一個線程中觀察另一個線程,所有的操作都是無序的。前半句是指線程內表現爲串行的語義;後半句是指指令重排序現象和工作內存與主內存同步延遲現象。Java語言提供了volatile和synchronized兩個關鍵字來保證線程之間操作的有序性。

關鍵字volatile可以說是Java虛擬機提供的最輕量級的同步機制,但不能說基於volatile變量的運算在併發下就是安全的。例如下面的一段代碼:

package cn.zhou;
/**
 * volatile變量自增運算測試
 * @author zhou
 *
 */
public class VolatileTest {
       public static volatile int race = 0;
       public static  void increase(){
                     race++;
       }
       /**
        *   public static synchronized void increase(){
                     race++;
       }
       is ok
        */
       public static final int THREADS_COUNT = 20;
       public static voidmain(String[] args) {
              Thread[] threads = newThread[THREADS_COUNT];
              for(inti=0;i<THREADS_COUNT;i++){
                     threads[i] = newThread(new Runnable() {
                           
                            @Override
                            public void run(){
                                   for(inti=0;i<1000;i++)
                                          increase();
                            }
                     });
                     threads[i].start();
              }
       /*    for(int i=0;i<THREADS_COUNT;i++){
                     try {
                            threads[i].join();
                     } catch(InterruptedException e) {
                            e.printStackTrace();
                     }
              } is ok
if use CountDownLatch is also ok
              */
//等待所有累加線程都結束
       while(Thread.activeCount()>1){
                     try {
                            Thread.sleep(10);
                     } catch(InterruptedException e) {
                            e.printStackTrace();
                     }
                                   //    Thread.yield();is also ok
              }
             
              System.out.println("race="+race);
       }
      
}


分析:此段代碼發起了20個線程,每個線程對race變量進行1000次自增運算,如果這段代碼能夠正確併發的話,那最後輸出的結果應該都是20000,但是運行時會發現,輸出的結果可能存在小於200000的情況。(如進行10000次運算,效果更明顯)。問題就在於自增運算“race++”中,因爲利用javap反編譯後會發現increase()方法在Class文件中是由四條字節碼指令構成的,主要看其中三條,getstatic iadd putstatic. 從字節碼層面上分析併發失敗的原因:當getstatic指令把race的值取到操作棧頂時,volatile關鍵字保證了race的值在此時是正確的,但是在執行iadd指令時其他線程可能已經把race變量的值加大了,而在操作棧頂的值就變成過期了的數據,所以putstatic指令執行後就可能把較小的race值同步到主內存中。(當然瞭如將increase()方法加上synchronized則可以保證併發正確)

         由於volatile變量只能保證可見性,在不符合下面兩條規則的運算場景中,我們仍然需要通過加鎖(使用synchronized或java.util.concurrent中的原子類如Lock/ReentrantLock)來保證原子性。

²  運算結果並不依賴變量的當前值,或者能夠確保只有單一的線程能修改變量的值

²  變量不需要與其他的狀態變量共同參與不變約束


二、線程相關小結

1、java虛擬機採用的是搶佔式的線程調度模型,指讓可運行,池中優先級高的線程佔用CPU,如果線程的優先級,那麼就隨機的選擇一個線程,使其佔用CPU。如果希望明確地讓一個線程給另一個線程運行的機會,可以採取以下辦法之一:

l  調整各個線程的優先級

l  讓處於運行狀態的線程調用Thread.sleep()方法 (稍加配合Thread.activeCount())

l  讓處於運行狀態的線程調用Thread.yield()(稍加配合Thread.activeCount())

l  讓處於運行狀態的線程調用另一個線程的join方法

2、線程互斥:使用同步對象鎖synchronized或者顯式的Lock對象/ReentrantLock(java.util.concurrent.*包中的鎖)--(lock(0與unlock()方法配合try/finally語句塊來實現)

3、線程同步:(1)wait/notify/notifyAll (都是Object的方法,它們必須在synchronized的代碼塊中且wait一般在while循環中) (2)使用java1.5 Concurrent包中提供的CountDownLatch(await和countDown方法)(3)join方法(Thread的一個方法,yieldsleep也是)

 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章