1. 程式人生 > >JAVA併發-從快取一致性說volatile

JAVA併發-從快取一致性說volatile

學過計算機組成原理的一定知道,為了解決記憶體速度跟不上CPU速度這個問題,在CPU的設計中加入了快取機制,快取的速度介於CPU和主存之間。在進行運算的時候,CPU將需要的資料對映一份在快取中,然後直接操作位於快取中的資料,操作完畢後再將快取中的資料寫回到主存。這在單執行緒環境中是沒有任何問題的。但是在多執行緒環境中就大不同了。
假設現在有這樣的一個場景:有兩個執行緒thread1和thread2,他們都在操作位於主存上的一個數據int a=2(具體操作為讀取a的值並執行一個自增操作)。邏輯上正確的結果:應當是最後a=4。但可能有這樣的情況,thread1將a=2從主存對映到自己的工作記憶體上,自增後變成a=3,在將a=3從工作記憶體寫回到主存之前,thread2也將a=2從從主存對映到自己的工作記憶體上,也自增後變成a=3。然後兩個執行緒先後將a=3寫回到主存上。顯然,a=3不是我們想看到的。看,這就是一個常見的快取一致性問題。兩個執行緒對a的操作結果互不可見,thread1不知道thread2對a進行了自增,thread2也不知道thread1對a進行了自增。在多執行緒程式設計中就是會出現這樣一致性的問題。(在JMM中,可以知道,記憶體分為主記憶體和工作記憶體,每個執行緒有自己 的工作記憶體,他們共享主記憶體)。
因此我們要辦法讓執行緒對共享變數的操作結果互相可見,java語言中的volatile關鍵字就幹了一件這樣的事。使用volatile修飾的共享變數,當有執行緒修改了他的值的時候,他會立即強制將修改的值寫回到主存,並通知其他使用該共享變數的執行緒:他們的快取區中關於此變數的值已經失效。請重新從主存中讀取。
仔細閱讀volatile乾的事,一共有3點影響:
1 將修改的值強制重新整理到主存
2 通知其他相關執行緒變數已經失效
3 其它執行緒再使用變數的時候就會重新從主存讀取


這就解決了JAVA併發程式設計中的可見性問題。
可見性:當多個執行緒訪問同一個共享變數的時候,一個執行緒對該共享變數的修改能夠實時的被訪問該共享變數的其他執行緒知曉。
繼續說上面的那個例子,如果變數a被使用了volatile修飾,那麼在thread1中,當a變為3的時候,就會強制重新整理到主存。如果這個時候,thread2已經將a=2從從主存對映到快取上,那麼在對a進行自增操作以前,會重新到主存中讀取a=3,然後自增到a=4,然後寫回到主存。上面的過程很完美,但這樣是否保證了a最終的結果一定是4呢?未必。
繼續說上面的那個例子,如果變數a被使用了volatile修飾,那麼在thread1中,當a變為3的時候,就會強制重新整理到主存。如果這個時候,thread2已經將a=2從從主存對映到快取上並且已經做完了自增操作,此時a=3,那麼最終主存中a的值為3。
所以,如果我們想讓a的最終值是4,僅僅保證可見性是不夠的,還得保證原子性。也就是對於變數a的自增操作加鎖,保證任意一個時刻只有一個執行緒對a進行自增操作。可以說volatile是一種“輕量級的鎖”,它能保證鎖的可見性,但不能保證鎖的原子性。

volatile變數的一種典型用法,就是用於那些狀態的標記,比如:

volatile boolean flag=false;
while(!flag){
    doSomething();
}


在其他執行緒中,可能會修改flag的值為true,代表退出迴圈。如果不使用volatile修飾flag,可能在flag被回收之後,主執行緒還沒收到其值改變的訊息。這是volatile的一種典型應用。當然我們也可以使用volatile型別的不可變物件來快取最新的內容。對於上一篇部落格http://yizhenn.iteye.com/blog/2286623講的那個例子:根據一個請求的1-9的阿拉伯數字返回對應的大寫漢字的servlet類。

public class OneValueCache{
    private final Integer lastNum;
    private final String lastStr;
    public OneValueCache(Integer num,String str){
        this.lastNum=new Integer(num);
        this.lastStr=new String(str);
    }
    public String getLastStr(int num){
         if(num==null || !num.equles(lastStr))
                return null;
         else
                return new String(lastStr);
    }
} 
@ThreadSafe  
public class MyServlet implements Servlet  
{  
private volatile OneValueCache cache=new OneValueCache(null,null);
 
   public void service(ServletRequest req,ServletResponse res){  
   int i=getNum(req);  
   String str=cache.getLastStr(i);  
   if(str==null){
     str=getHanziByNum(i)
     cache=new OneValueCache(i,str);
   }
   responseHanzi(res,str);  
}  
} 

筆者開設了一個知乎live,詳細的介紹的JAVA從入門到精通該如何學,學什麼?