溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

JAVA并發(fā)中VOLATILE關鍵字的示例分析

發(fā)布時間:2021-05-12 10:27:40 來源:億速云 閱讀:148 作者:小新 欄目:開發(fā)技術

小編給大家分享一下JAVA并發(fā)中VOLATILE關鍵字的示例分析,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!

并發(fā)編程中的三個概念:

1.原子性

在Java中,對基本數(shù)據(jù)類型的變量的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的,要么執(zhí)行,要么不執(zhí)行。

2.可見性

對于可見性,Java提供了volatile關鍵字來保證可見性。

  當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內(nèi)存中讀取新值。

  而普通的共享變量不能保證可見性,因為普通共享變量被修改之后,什么時候被寫入主存是不確定的,當其他線程去讀取時,此時內(nèi)存中可能還是原來的舊值,因此無法保證可見性。

  另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然后執(zhí)行同步代碼,并且在釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。

3.有序性

在Java內(nèi)存模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執(zhí)行,卻會影響到多線程并發(fā)執(zhí)行的正確性。

  在Java里面,可以通過volatile關鍵字來保證一定的“有序性”(具體原理在下一節(jié)講述)。另外可以通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個時刻是有一個線程執(zhí)行同步代碼,相當于是讓線程順序執(zhí)行同步代碼,自然就保證了有序性。

  另外,Java內(nèi)存模型具備一些先天的“有序性”,即不需要通過任何手段就能夠得到保證的有序性,這個通常也稱為 happens-before 原則。如果兩個操作的執(zhí)行次序無法從happens-before原則推導出來,那么它們就不能保證它們的有序性,虛擬機可以隨意地對它們進行重排序。

volatile關鍵字的兩層語義

  一旦一個共享變量(類的成員變量、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語義:

  1)保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。

  2)禁止進行指令重排序。

  先看一段代碼,假如線程1先執(zhí)行,線程2后執(zhí)行:

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

這段代碼是很典型的一段代碼,很多人在中斷線程時可能都會采用這種標記辦法。但是事實上,這段代碼會完全運行正確么?即一定會將線程中斷么?不一定,也許在大多數(shù)時候,這個代碼能夠把線程中斷,但是也有可能會導致無法中斷線程(雖然這個可能性很小,但是只要一旦發(fā)生這種情況就會造成死循環(huán)了)。

  下面解釋一下這段代碼為何有可能導致無法中斷線程。在前面已經(jīng)解釋過,每個線程在運行過程中都有自己的工作內(nèi)存,那么線程1在運行的時候,會將stop變量的值拷貝一份放在自己的工作內(nèi)存當中。

  那么當線程2更改了stop變量的值之后,但是還沒來得及寫入主存當中,線程2轉去做其他事情了,那么線程1由于不知道線程2對stop變量的更改,因此還會一直循環(huán)下去。

  但是用volatile修飾之后就變得不一樣了:

  第一:使用volatile關鍵字會強制將修改的值立即寫入主存;

  第二:使用volatile關鍵字的話,當線程2進行修改時,會導致線程1的工作內(nèi)存中緩存變量stop的緩存行無效(反映到硬件層的話,就是CPU的L1或者L2緩存中對應的緩存行無效);

  第三:由于線程1的工作內(nèi)存中緩存變量stop的緩存行無效,所以線程1再次讀取變量stop的值時會去主存讀取。

  那么在線程2修改stop值時(當然這里包括2個操作,修改線程2工作內(nèi)存中的值,然后將修改后的值寫入內(nèi)存),會使得線程1的工作內(nèi)存中緩存變量stop的緩存行無效,然后線程1讀取時,發(fā)現(xiàn)自己的緩存行無效,它會等待緩存行對應的主存地址被更新之后,然后去對應的主存讀取最新的值。

  那么線程1讀取到的就是最新的正確的值。

2.volatile保證原子性嗎?

  從上面知道volatile關鍵字保證了操作的可見性,但是volatile能保證對變量的操作是原子性嗎?

  下面看一個例子:

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 )   //保證前面的線程都執(zhí)行完
             Thread.yield();
         System.out.println(test.inc);
     }
}

  大家想一下這段程序的輸出結果是多少?也許有些朋友認為是10000。但是事實上運行它會發(fā)現(xiàn)每次運行結果都不一致,都是一個小于10000的數(shù)字。

  可能有的朋友就會有疑問,不對啊,上面是對變量inc進行自增操作,由于volatile保證了可見性,那么在每個線程中對inc自增完之后,在其他線程中都能看到修改后的值啊,所以有10個線程分別進行了1000次操作,那么最終inc的值應該是1000*10=10000。

  這里面就有一個誤區(qū)了,volatile關鍵字能保證可見性沒有錯,但是上面的程序錯在沒能保證原子性??梢娦灾荒鼙WC每次讀取的是最新的值,但是volatile沒辦法保證對變量的操作的原子性。

  在前面已經(jīng)提到過,自增操作是不具備原子性的,它包括讀取變量的原始值、進行加1操作、寫入工作內(nèi)存。那么就是說自增操作的三個子操作可能會分割開執(zhí)行,就有可能導致下面這種情況出現(xiàn):

  假如某個時刻變量inc的值為10,

  線程1對變量進行自增操作,線程1先讀取了變量inc的原始值,然后線程1被阻塞了;

  然后線程2對變量進行自增操作,線程2也去讀取變量inc的原始值,由于線程1只是對變量inc進行讀取操作,而沒有對變量進行修改操作,所以不會導致線程2的工作內(nèi)存中緩存變量inc的緩存行無效,所以線程2會直接去主存讀取inc的值,發(fā)現(xiàn)inc的值時10,然后進行加1操作,并把11寫入工作內(nèi)存,最后寫入主存。

  然后線程1接著進行加1操作,由于已經(jīng)讀取了inc的值,注意此時在線程1的工作內(nèi)存中inc的值仍然為10,所以線程1對inc進行加1操作后inc的值為11,然后將11寫入工作內(nèi)存,最后寫入主存。

  那么兩個線程分別進行了一次自增操作后,inc只增加了1。

  解釋到這里,可能有朋友會有疑問,不對啊,前面不是保證一個變量在修改volatile變量時,會讓緩存行無效嗎?然后其他線程去讀就會讀到新的值,對,這個沒錯。這個就是上面的happens-before規(guī)則中的volatile變量規(guī)則,但是要注意,線程1對變量進行讀取操作之后,被阻塞了的話,并沒有對inc值進行修改。然后雖然volatile能保證線程2對變量inc的值讀取是從內(nèi)存中讀取的,但是線程1沒有進行修改,所以線程2根本就不會看到修改的值。

  根源就在這里,自增操作不是原子性操作,而且volatile也無法保證對變量的任何操作都是原子性的。

  在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作類,即對基本數(shù)據(jù)類型的 自增(加1操作),自減(減1操作)、以及加法操作(加一個數(shù)),減法操作(減一個數(shù))進行了封裝,保證這些操作是原子性操作。atomic是利用CAS來實現(xiàn)原子性操作的(Compare And Swap),CAS實際上是利用處理器提供的CMPXCHG指令實現(xiàn)的,而處理器執(zhí)行CMPXCHG指令是一個原子性操作。

3.volatile能保證有序性嗎?

  在前面提到volatile關鍵字能禁止指令重排序,所以volatile能在一定程度上保證有序性。

  volatile關鍵字禁止指令重排序有兩層意思:

  1)當程序執(zhí)行到volatile變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經(jīng)進行,且結果已經(jīng)對后面的操作可

最后一點

CAS原理

由于 volatile 關鍵字不具有原子性,所以一般在使用 volatile 關鍵字的地方,常常出現(xiàn) CAS。

CAS是 Compare And Swap,它和 volatile 關鍵字都是實現(xiàn) JUC 的基礎,其中 java.util.concurrent.atomic 核心都是 CAS 。

使用 CAS 有兩個核心參數(shù),第一個是舊值,第二個是期望值。根據(jù)當前類(this)和 內(nèi)存偏移(valueOffset)計算出內(nèi)存中的值,當內(nèi)存中的值和舊值相等時,更新為新值并返回 true ,否則返回 false。

比如 AtomicInteger 類中的 CompareAndSet() 方法:

public final boolean compareAndSet(int expect, int update) {
 
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
 
}

根據(jù) this 和 valueOffset 計算出的值與 expect 是否相等,相等把內(nèi)存中的值更新為 update 并返回 true ,否則返回 false 。

說明了這些線程安全的包裝類的底層都是用到了volatile關鍵字做線程安全的保證

Java是什么

Java是一門面向對象編程語言,可以編寫桌面應用程序、Web應用程序、分布式系統(tǒng)和嵌入式系統(tǒng)應用程序。

以上是“JAVA并發(fā)中VOLATILE關鍵字的示例分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業(yè)資訊頻道!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI