Java 中的volitle 關鍵字

轉自:https://www.cnblogs.com/prctice/p/4434968.html

研究ThreadPoolExecutor的時候,發現其中大量使用了volatile變量。不知爲何,因此做了一番查找,研究: 其中借鑑了很多網上資料。 在瞭解volatile變量作用前,先需要明白一些概念: 

什麼是原子操作? 
所謂原子操作,就是"不可中斷的一個或一系列操作" , 在確認一個操作是原子的情況下,多線程環境裏面,我們可以避免僅僅爲保護這個操作在外圍加上性能昂貴的鎖,甚至藉助於原子操作,我們可以實現互斥鎖。 很多操作系統都爲int類型提供了+-賦值的原子操作版本,比如 NT 提供了 InterlockedExchange 等API, Linux/UNIX也提供了atomic_set 等函數。 

關於java中的原子性? 
原子性可以應用於除long和double之外的所有基本類型之上的“簡單操作”。對於讀取和寫入出long double之外的基本類型變量這樣的操作,可以保證它們會被當作不可分(原子)的操作來操作。 因爲JVM的版本和其它的問題,其它的很多操作就不好說了,比如說++操作在C++中是原子操作,但在Java中就不好說了。 另外,Java提供了AtomicInteger等原子類。再就是用原子性來控制併發比較麻煩,也容易出問題。 

volatile原理是什麼? 
Java中volatile關鍵字原義是“不穩定、變化”的意思 
使用volatile和不使用volatile的區別在於JVM內存主存和線程工作內存的同步之上。volatile保證變量在線程工作內存和主存之間一致。 
其實是告訴處理器, 不要將我放入工作內存, 請直接在主存操作我. 
實現原理:

Volatile的實現原理

那麼Volatile是如何來保證可見性的呢?在x86處理器下通過工具獲取JIT編譯器生成的彙編指令來看看對Volatile進行寫操作CPU會做什麼事情。

Java代碼: instance = new Singleton();//instance是volatile變量
彙編代碼: 0x01a3de1d: movb $0x0,0x1104800(%esi);0x01a3de24: lock addl $0x0,(%esp);

有volatile變量修飾的共享變量進行寫操作的時候會多第二行彙編代碼,通過查IA-32架構軟件開發者手冊可知,lock前綴的指令在多核處理器下會引發了兩件事情。

  • 將當前處理器緩存行的數據會寫回到系統內存。
  • 這個寫回內存的操作會引起在其他CPU裏緩存了該內存地址的數據無效。

處理器爲了提高處理速度,不直接和內存進行通訊,而是先將系統內存的數據讀到內部緩存(L1,L2或其他)後再進行操作,但操作完之後不知道何時會寫到內存,如果對聲明瞭Volatile變量進行寫操作,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。但是就算寫回到內存,如果其他處理器緩存的值還是舊的,再執行計算操作就會有問題,所以在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器要對這個數據進行修改操作的時候,會強制重新從系統內存裏把數據讀到處理器緩存裏。

這兩件事情在IA-32軟件開發者架構手冊的第三冊的多處理器管理章節(第八章)中有詳細闡述。

Lock前綴指令會引起處理器緩存回寫到內存。Lock前綴指令導致在執行指令期間,聲言處理器的 LOCK# 信號。在多處理器環境中,LOCK# 信號確保在聲言該信號期間,處理器可以獨佔使用任何共享內存。(因爲它會鎖住總線,導致其他CPU不能訪問總線,不能訪問總線就意味着不能訪問系統內存),但是在最近的處理器裏,LOCK#信號一般不鎖總線,而是鎖緩存,畢竟鎖總線開銷比較大。在8.1.4章節有詳細說明鎖定操作對處理器緩存的影響,對於Intel486和Pentium處理器,在鎖操作時,總是在總線上聲言LOCK#信號。但在P6和最近的處理器中,如果訪問的內存區域已經緩存在處理器內部,則不會聲言LOCK#信號。相反地,它會鎖定這塊內存區域的緩存並回寫到內存,並使用緩存一致性機制來確保修改的原子性,此操作被稱爲“緩存鎖定”,緩存一致性機制會阻止同時修改被兩個以上處理器緩存的內存區域數據

一個處理器的緩存回寫到內存會導致其他處理器的緩存無效。IA-32處理器和Intel 64處理器使用MESI(修改,獨佔,共享,無效)控制協議去維護內部緩存和其他處理器緩存的一致性。在多核處理器系統中進行操作的時候,IA-32 和Intel 64處理器能嗅探其他處理器訪問系統內存和它們的內部緩存。它們使用嗅探技術保證它的內部緩存,系統內存和其他處理器的緩存的數據在總線上保持一致。例如在Pentium和P6 family處理器中,如果通過嗅探一個處理器來檢測其他處理器打算寫內存地址,而這個地址當前處理共享狀態,那麼正在嗅探的處理器將無效它的緩存行,在下次訪問相同內存地址時,強制執行緩存行填充。


接下來是測試 :(通過測試能更好的發現和分析問題) 
申明瞭幾種整形的變量,開啓100個線程同時對這些變量進行++操作,發現結果差異很大: 
>>Execute End: 
>>Atomic: 100000 
>>VInteger: 38790 
>>Integer: 68749 
>>Source i: 99205 
>>Source Vi: 99286 
也就是說除了Atomic,其他的都是錯誤的。 

我們通過一些疑問,來解釋一下。 

1:爲什麼會產生錯誤的數據? 
多線程引起的,因爲對於多線程同時操作一個整型變量在大併發操作的情況下無法做到同步,而Atom提供了很多針對此類線程安全問題的解決方案,因此解決了同時讀寫操作的問題。


2:爲什麼會造成同步問題? 
Java多線程在對變量進行操作的時候,實際上是每個線程會單獨分配一個針對i值的拷貝(獨立內存區域),但是申明的i值確是在主內存區域中,當對i值修改完畢後,線程會將自己內存區域塊中的i值拷貝到主內存區域中,因此有可能每個線程拿到的i值是不一樣的,從而出現了同步問題。


3:爲什麼使用volatile修飾integer變量後,還是不行? 
因爲volatile僅僅只是解決了存儲的問題,即i值只是保留在了一個內存區域中,但是i++這個操作,涉及到獲取i值、修改i值、存儲i值(i=i+1),這裏的volatile只是解決了存儲i值得問題,至於獲取和修改i值,確是沒有做到同步。


4:既然不能做到同步,那爲什麼還要用volatile這種修飾符? 
主要的一個原因是方便,因爲只需添加一個修飾符即可,而無需做對象加鎖、解鎖這麼麻煩的操作。但是本人不推薦使用這種機制,因爲比較容易出問題(髒數據),而且也保證不了同步。


5:那到底如何解決這樣的問題? 
        第一種:採用同步synchronized解決,這樣雖然解決了問題,但是也降低了系統的性能。 
        第二種:採用原子性數據Atomic變量,這是從JDK1.5開始才存在的針對原子性的解決方案,這種方案也是目前比較好的解決方案了。


6:Atomic的實現基本原理? 
首先Atomic中的變量是申明爲了volatile變量的,這樣就保證的變量的存儲和讀取是一致的,都是來自同一個內存塊,然後Atomic提供了getAndIncrement方法,該方法對變量的++操作進行了封裝,並提供了compareAndSet方法,來完成對單個變量的加鎖和解鎖操作,方法中用到了一個UnSafe的對象,現在還不知道這個UnSafe的工作原理(似乎沒有公開源代碼)。Atomic雖然解決了同步的問題,但是性能上面還是會有所損失,不過影響不大,網上有針對這方面的測試,大概50million的操作對比是250ms : 850ms,對於大部分的高性能應用,應該還是夠的了。

package qflag.ucstar.test.thread;   

  

import java.util.concurrent.atomic.AtomicInteger;    
   
/** 
* 測試原子性的同步 
* @author polarbear 2009-3-14 
* 
*/   
public class TestAtomic {    
        
    public static AtomicInteger astom_i = new AtomicInteger();    
        
    public static volatile Integer v_integer_i = 0;    
        
    public static volatile int v_i = 0;    
        
    public static Integer integer_i = 0;    
        
    public static int i = 0;    
        
    public static int endThread = 0;    
        
    public static void main(String[] args) {    
        new TestAtomic().testAtomic();    
     }    
        
    public void testAtomic() {    
            
        for(int i=0; i<100; i++) {    
            new Thread(new IntegerTestThread()).start();    
         }    
            
        try {    
            for(;;) {    
                 Thread.sleep(500);    
                if(TestAtomic.endThread == 100) {    
                     System.out.println(">>Execute End:");    
                     System.out.println(">>Atomic: /t"+TestAtomic.astom_i);    
                     System.out.println(">>VInteger: /t"+TestAtomic.v_integer_i);    
                     System.out.println(">>Integer: /t"+TestAtomic.integer_i);    
                     System.out.println(">>Source i: /t"+TestAtomic.i);    
                     System.out.println(">>Source Vi: /t"+TestAtomic.v_i);    
                    break;    
                 }    
             }    
                
         } catch (Exception e) {    
             e.printStackTrace();    
         }    
     }    
        
}    
class IntegerTestThread implements Runnable {    
    public void run() {    
        int x = 0;    
        while(x<1000) {    
             TestAtomic.astom_i.incrementAndGet();    
             TestAtomic.v_integer_i++;    
             TestAtomic.integer_i++;    
             TestAtomic.i++;    
             TestAtomic.v_i++;    
             x++;    
         }    
         ++TestAtomic.endThread;    //貌似很無敵!難道是原子性的嗎? 
     }    
}

-----------------------------------------xx-----------------------------------xx-----------------------------------------------

 

 

本人繼續補充:

除了TestAtomic.endThread,其他的變量都被忽略了。具體解釋可參見注釋。
 

import java.util.concurrent.atomic.AtomicInteger; 
import java.io.*;

/** 
* 測試原子性的同步 
* @author pyc 2009-3-29 
* 
*/ 
public class TestAtomic {    
    public static final int N=10;
    public static final int M=10000;
    public static int perfect_result=M*N;
    public static int endThread = 0; 
    
private PrintWriter out;//將信息輸入至文本"out.txt",因爲控制檯buffer可能不夠.

public TestAtomic() throws IOException
{
   out =new PrintWriter(
     new BufferedWriter(
       new FileWriter("out.txt")));
} 
       
    public static void main(String[] args) { 
         try{ 
         new TestAtomic().testAtomic(); 
         }catch(Exception e){
         System.out.println(e.getMessage());
          }
         System.out.println("OK./nStatistical report:");
         System.out.println("Covered by "+(perfect_result-endThread)+" times.");
    }   
       
    public void testAtomic() { 
        Thread[] td=new Thread[N];
        for(int i=0; i<N; i++) {   
        td[i]=new Thread(new IntegerTestThread(i+1));
        }   
        for(int i=0; i<N; i++) {   
        td[i].start();
        out.println((i+1)+" go..") ; //此處如果run()方法代碼少,立即可觀察到complete完成信息。
        }     
        try { 
        long temp=0; //存放了上次的endTread值。
        int count=1000; //如果temp值超過一千次的重複就可以認爲結束程序。
        for(;;) { 
           //Thread.sleep(1); //有可能main線程運行過快,可以調節採樣的頻率。
           if(TestAtomic.endThread == perfect_result) {   
            out.println("==============/r/nPerfect!/r/n=============="); //完美匹配!
                break; 
            }
            if(temp==TestAtomic.endThread){
               out.println("Equal!!");//有重複,有可能是所有線程運行結束時的重複,也有可能是main線程採樣過快。
               count--;//倒計時中。。。
            }
            else {
               temp=TestAtomic.endThread;//給temp賦新值。
               count=1000;//重新設置倒計時。
            }
            out.println("endThread = "+TestAtomic.endThread);//在此處有機率可觀察當前的endThread值比上次要少。
            //這是關鍵之處!
            if(count<=0)
            {
               out.println("/r/nI'll be crazy if I wait for that once again!/r/nFailed, OMG!+_+");
               break;
            }
        }   
        out.close();     
        }catch(Exception e) {   
            e.printStackTrace();   
        }   
    }   
    
    class IntegerTestThread implements Runnable { 
    private int id;
    public IntegerTestThread(int i){
       this.id=i;
    }
    public void run() {   
       int i=M;//充分保證線程重疊運行
       while(i>0){
        try{
         //Thread.sleep((int)(10*Math.random()));//設置睡眠時間,從而儘可能使線程重疊運行。
        }catch(Exception e){
        ++TestAtomic.endThread;//測試該語句的“原子”性。其實做完實驗,我們知道,++i,i++,   i=i+1一樣都不能保證原子性。
        //我們可以從最終的endThread值是不是等於M*N得知。
        i--;
       }   
       out.println("************/r/n"+id+" has Completed!/r/n************/r/n") ;
    }   
    } 
}

 

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