對volatile不具有原子性的理解


在閱讀多線程書籍的時候,對volatile的原子性產生了疑問,問題類似於這篇文章所闡述的那樣。經過一番思考給出自己的理解。
我們知道對於可見性,Java提供了volatile關鍵字來保證可見性有序性但不保證原子性
普通的共享變量不能保證可見性,因為普通共享變量被修改之后,什么時候被寫入主存是不確定的,當其他線程去讀取時,此時內存中可能還是原來的舊值,因此無法保證可見性。


  背景:為了提高處理速度,處理器不直接和內存進行通信,而是先將系統內存的數據讀到內部緩存(L1,L2或其他)后再進行操作,但操作完不知道何時會寫到內存。

  • 如果對聲明了volatile的變量進行寫操作,JVM就會向處理器發送一條指令,將這個變量所在緩存行的數據寫回到系統內存。但是,就算寫回到內存,如果其他處理器緩存的值還是舊的,再執行計算操作就會有問題。
  • 在多處理器下,為了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操作的時候,會重新從系統內存中把數據讀到處理器緩存里。

總結下來

  • 第一:使用volatile關鍵字會強制將修改的值立即寫入主存;
  • 第二:使用volatile關鍵字的話,當線程2進行修改時,會導致線程1的工作內存中緩存變量的緩存行無效(反映到硬件層的話,就是CPU的L1或者L2緩存中對應的緩存行無效);
  • 第三:由於線程1的工作內存中緩存變量的緩存行無效,所以線程1再次讀取變量的值時會去主存讀取。

最重要的是

  • 可見性:對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入。
  • 原子性:對任意單個volatile變量的讀/寫具有原子性,但類似於volatile++這種復合操作不具有原子性。

舉2個例子,例子來源於這篇文章:

例子是這樣的:

//線程1
boolean stop = false;
while(!stop){
    doSomething();
}
 
//線程2
stop = true;

原文:這段代碼是很典型的一段代碼,很多人在中斷線程時可能都會采用這種標記辦法。但是事實上,這段代碼會完全運行正確么?即一定會將線程中斷么?不一定,也許在大多數時候,這個代碼能夠把線程中斷,但是也有可能會導致無法中斷線程(雖然這個可能性很小,但是只要一旦發生這種情況就會造成死循環了)。
  下面解釋一下這段代碼為何有可能導致無法中斷線程。在前面已經解釋過,每個線程在運行過程中都有自己的工作內存,那么線程1在運行的時候,會將stop變量的值拷貝一份放在自己的工作內存當中。
  那么當線程2更改了stop變量的值之后,但是還沒來得及寫入主存當中,線程2轉去做其他事情了,那么線程1由於不知道線程2對stop變量的更改,因此還會一直循環下去。
  但是用volatile修飾之后就變得不一樣了:
  第一:使用volatile關鍵字會強制將修改的值立即寫入主存;
  第二:使用volatile關鍵字的話,當線程2進行修改時,會導致線程1的工作內存中緩存變量stop的緩存行無效(反映到硬件層的話,就是CPU的L1或者L2緩存中對應的緩存行無效);
  第三:由於線程1的工作內存中緩存變量stop的緩存行無效,所以線程1再次讀取變量stop的值時會去主存讀取。
到這里可能看起來沒什么問題,我們來看例子2

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(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }    
        while(Thread.activeCount()>1)  //保證前面的線程都執行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

原文:大家想一下這段程序的輸出結果是多少?也許有些朋友認為是10000。但是事實上運行它會發現每次運行結果都不一致,都是一個小於10000的數字。
  可能有的朋友就會有疑問,不對啊,上面是對變量inc進行自增操作,由於volatile保證了可見性,那么在每個線程中對inc自增完之后,在其他線程中都能看到修改后的值啊,所以有10個線程分別進行了1000次操作,那么最終inc的值應該是1000*10=10000。
  這里面就有一個誤區了,volatile關鍵字能保證可見性沒有錯,但是上面的程序錯在沒能保證原子性。可見性只能保證每次讀取的是最新的值,但是volatile沒辦法保證對變量的操作的原子性。
  在前面已經提到過,自增操作是不具備原子性的,它包括讀取變量的原始值、進行加1操作、寫入工作內存。那么就是說自增操作的三個子操作可能會分割開執行,就有可能導致下面這種情況出現:
  假如某個時刻變量inc的值為10,
  線程1對變量進行自增操作,線程1先讀取了變量inc的原始值,然后線程1被阻塞了;
  然后線程2對變量進行自增操作,線程2也去讀取變量inc的原始值,由於線程1只是對變量inc進行讀取操作,而沒有對變量進行修改操作,所以不會導致線程2的工作內存中緩存變量inc的緩存行無效,所以線程2會直接去主存讀取inc的值,發現inc的值時10,然后進行加1操作,並把11寫入工作內存,最后寫入主存。
  然后線程1接着進行加1操作,由於已經讀取了inc的值,注意此時在線程1的工作內存中inc的值仍然為10,所以線程1對inc進行加1操作后inc的值為11,然后將11寫入工作內存,最后寫入主存。
  那么兩個線程分別進行了一次自增操作后,inc只增加了1。
  解釋到這里,可能有朋友會有疑問,不對啊,前面不是保證一個變量在修改volatile變量時,會讓緩存行無效嗎?然后其他線程去讀就會讀到新的值,對,這個沒錯。這個就是上面的happens-before規則中的volatile變量規則,但是要注意,線程1對變量進行讀取操作之后,被阻塞了的話,並沒有對inc值進行修改。然后雖然volatile能保證線程2對變量inc的值讀取是從內存中讀取的,但是線程1沒有進行修改,所以線程2根本就不會看到修改的值。

大家是不是有這樣的疑問:“線程1在讀取inc為10后被阻塞了,沒有進行修改所以不會去通知其他線程,此時線程2拿到的還是10,這點可以理解。但是后來線程2修改了inc變成11后寫回主內存,這下是修改了,線程1再次運行時,難道不會去主存中獲取最新的值嗎?按照volatile的定義,如果volatile修飾的變量發生了變化,其他線程應該去主存中拿變化后的值才對啊?”
  是不是還有:例子1中線程1先將stop=flase讀取到了工作內存中,然后去執行循環操作,線程2將stop=true寫入到主存后,為什么線程1的工作內存中stop=false會變成無效的?

其實嚴格的說,對任意單個volatile變量的讀/寫具有原子性,但類似於volatile++這種復合操作不具有原子性。在《Java並發編程的藝術》中有這一段描述:“在多處理器下,為了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操作的時候,會重新從系統內存中把數據讀到處理器緩存里。”我們需要注意的是,這里的修改操作,是指的一個操作

  • 例子1中,因為是while語句,線程會不斷讀取stop的值來判斷是否為false,每一次判斷都是一個操作。這里是從緩存中讀取。單個讀取操作是具有原子性的,所以當例子1中的線程2修改了stop時,由於volatile變量的可見性,線程1再讀取stop時是最新的值,為true。
  • 而例子2中,為什么自增操作會出現那樣的結果呢?可以知道自增操作是三個原子操作組合而成的復合操作。在一個操作中,讀取了inc變量后,是不會再讀取的inc的,所以它的值還是之前讀的10,它的下一步是自增操作。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM