欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

深入淺析Java中的volatile

 更新時間:2017年03月19日 09:50:33   作者:dreamcatcher-cx  
volatile是Java提供的一種輕量級的同步機制,在并發(fā)編程中,它也扮演著比較重要的角色.這篇文章主要介紹了深入淺析Java中的volatile,需要的朋友可以參考下

內存可見性

  volatile是Java提供的一種輕量級的同步機制,在并發(fā)編程中,它也扮演著比較重要的角色。同synchronized相比(synchronized通常稱為重量級鎖),volatile更輕量級,相比使用synchronized時引起的線程上下文切換所帶來的龐大開銷,倘若能恰當的合理的使用volatile,自然是美事一樁。

  為了能比較清晰徹底的理解volatile,我們一步一步來分析。首先來看看如下代碼

public class TestVolatile {
 boolean status = false;
 /**
  * 狀態(tài)切換為true
  */
 public void changeStatus(){
  status = true;
 }
 /**
  * 若狀態(tài)為true,則running。
  */
 public void run(){
  if(status){
   System.out.println("running....");
  }
 }
}

  上面這個例子,在多線程環(huán)境里,假設線程A執(zhí)行changeStatus()方法后,線程B運行run()方法,可以保證輸出"running....."嗎?

  答案是NO!

  這個結論會讓人有些疑惑,可以理解。因為倘若在單線程模型里,先運行changeStatus方法,再執(zhí)行run方法,自然是可以正確輸出"running...."的;但是在多線程模型中,是沒法做這種保證的。因為對于共享變量status來說,線程A的修改,對于線程B來講,是"不可見"的。也就是說,線程B此時可能無法觀測到status已被修改為true。那么什么是可見性呢?

  所謂可見性,是指當一條線程修改了共享變量的值,新值對于其他線程來說是可以立即得知的。很顯然,上述的例子中是沒有辦法做到內存可見性的。

  Java內存模型

  為什么出現這種情況呢,我們需要先了解一下JMM(java內存模型)

  java虛擬機有自己的內存模型(Java Memory Model,JMM),JMM可以屏蔽掉各種硬件和操作系統(tǒng)的內存訪問差異,以實現讓java程序在各種平臺下都能達到一致的內存訪問效果。

  JMM決定一個線程對共享變量的寫入何時對另一個線程可見,JMM定義了線程和主內存之間的抽象關系:共享變量存儲在主內存(Main Memory)中,每個線程都有一個私有的本地內存(Local Memory),本地內存保存了被該線程使用到的主內存的副本拷貝,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存中的變量。這三者之間的交互關系如下

 

  需要注意的是,JMM是個抽象的內存模型,所以所謂的本地內存,主內存都是抽象概念,并不一定就真實的對應cpu緩存和物理內存。當然如果是出于理解的目的,這樣對應起來也無不可。

  大概了解了JMM的簡單定義后,問題就很容易理解了,對于普通的共享變量來講,比如我們上文中的status,線程A將其修改為true這個動作發(fā)生在線程A的本地內存中,此時還未同步到主內存中去;而線程B緩存了status的初始值false,此時可能沒有觀測到status的值被修改了,所以就導致了上述的問題。那么這種共享變量在多線程模型中的不可見性如何解決呢?比較粗暴的方式自然就是加鎖,但是此處使用synchronized或者Lock這些方式太重量級了,有點炮打蚊子的意思。比較合理的方式其實就是volatile

  volatile具備兩種特性,第一就是保證共享變量對所有線程的可見性。將一個共享變量聲明為volatile后,會有以下效應:

    1.當寫一個volatile變量時,JMM會把該線程對應的本地內存中的變量強制刷新到主內存中去;

    2.這個寫會操作會導致其他線程中的緩存無效。

上面的例子只需將status聲明為volatile,即可保證在線程A將其修改為true時,線程B可以立刻得知

 volatile boolean status = false;

留意復合類操作

  但是需要注意的是,我們一直在拿volatile和synchronized做對比,僅僅是因為這兩個關鍵字在某些內存語義上有共通之處,volatile并不能完全替代synchronized,它依然是個輕量級鎖,在很多場景下,volatile并不能勝任??聪逻@個例子:

package test;
import java.util.concurrent.CountDownLatch;
/**
 * Created by chengxiao on 2017/3/18.
 */
public class Counter {
 public static volatile int num = 0;
 //使用CountDownLatch來等待計算線程執(zhí)行完
 static CountDownLatch countDownLatch = new CountDownLatch(30);
 public static void main(String []args) throws InterruptedException {
  //開啟30個線程進行累加操作
  for(int i=0;i<30;i++){
   new Thread(){
    public void run(){
     for(int j=0;j<10000;j++){
      num++;//自加操作
     }
     countDownLatch.countDown();
    }
   }.start();
  }
  //等待計算線程執(zhí)行完
  countDownLatch.await();
  System.out.println(num);
 }
}

執(zhí)行結果:

224291

針對這個示例,一些同學可能會覺得疑惑,如果用volatile修飾的共享變量可以保證可見性,那么結果不應該是300000么?

問題就出在num++這個操作上,因為num++不是個原子性的操作,而是個復合操作。我們可以簡單講這個操作理解為由這三步組成:

  1.讀取

  2.加一

  3.賦值

  所以,在多線程環(huán)境下,有可能線程A將num讀取到本地內存中,此時其他線程可能已經將num增大了很多,線程A依然對過期的num進行自加,重新寫到主存中,最終導致了num的結果不合預期,而是小于30000。

解決num++操作的原子性問題

  針對num++這類復合類的操作,可以使用java并發(fā)包中的原子操作類原子操作類是通過循環(huán)CAS的方式來保證其原子性的。

/**
 * Created by chengxiao on 2017/3/18.
 */
public class Counter {
  //使用原子操作類
 public static AtomicInteger num = new AtomicInteger(0);
 //使用CountDownLatch來等待計算線程執(zhí)行完
 static CountDownLatch countDownLatch = new CountDownLatch(30);
 public static void main(String []args) throws InterruptedException {
  //開啟30個線程進行累加操作
  for(int i=0;i<30;i++){
   new Thread(){
    public void run(){
     for(int j=0;j<10000;j++){
      num.incrementAndGet();//原子性的num++,通過循環(huán)CAS方式
     }
     countDownLatch.countDown();
    }
   }.start();
  }
  //等待計算線程執(zhí)行完
  countDownLatch.await();
  System.out.println(num);
 }
}

執(zhí)行結果

300000
關于原子類操作的基本原理,會在后面的章節(jié)進行介紹,此處不再贅述。

禁止指令重排序

volatile還有一個特性:禁止指令重排序優(yōu)化。

重排序是指編譯器和處理器為了優(yōu)化程序性能而對指令序列進行排序的一種手段。但是重排序也需要遵守一定規(guī)則:

  1.重排序操作不會對存在數據依賴關系的操作進行重排序。

    比如:a=1;b=a; 這個指令序列,由于第二個操作依賴于第一個操作,所以在編譯時和處理器運行時這兩個操作不會被重排序。

  2.重排序是為了優(yōu)化性能,但是不管怎么重排序,單線程下程序的執(zhí)行結果不能被改變

    比如:a=1;b=2;c=a+b這三個操作,第一步(a=1)和第二步(b=2)由于不存在數據依賴關系,所以可能會發(fā)生重排序,但是c=a+b這個操作是不會被重排序的,因為需要保證最終的結果一定是c=a+b=3。

  重排序在單線程模式下是一定會保證最終結果的正確性,但是在多線程環(huán)境下,問題就出來了,來開個例子,我們對第一個TestVolatile的例子稍稍改進,再增加個共享變量a

public class TestVolatile {
 int a = 1;
 boolean status = false;
 /**
  * 狀態(tài)切換為true
  */
 public void changeStatus(){
  a = 2;//1
  status = true;//2
 }
 /**
  * 若狀態(tài)為true,則running。
  */
 public void run(){
  if(status){//3
   int b = a+1;//4
   System.out.println(b);
  }
 }
}

  假設線程A執(zhí)行changeStatus后,線程B執(zhí)行run,我們能保證在4處,b一定等于3么?

  答案依然是無法保證!也有可能b仍然為2。上面我們提到過,為了提供程序并行度,編譯器和處理器可能會對指令進行重排序,而上例中的1和2由于不存在數據依賴關系,則有可能會被重排序,先執(zhí)行status=true再執(zhí)行a=2。而此時線程B會順利到達4處,而線程A中a=2這個操作還未被執(zhí)行,所以b=a+1的結果也有可能依然等于2。

  使用volatile關鍵字修飾共享變量便可以禁止這種重排序。若用volatile修飾共享變量,在編譯時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序

  volatile禁止指令重排序也有一些規(guī)則,簡單列舉一下:

  1.當第二個操作是voaltile寫時,無論第一個操作是什么,都不能進行重排序

  2.當地一個操作是volatile讀時,不管第二個操作是什么,都不能進行重排序

  3.當第一個操作是volatile寫時,第二個操作是volatile讀時,不能進行重排序

總結:

  簡單總結下,volatile是一種輕量級的同步機制,它主要有兩個特性:一是保證共享變量對所有線程的可見性;二是禁止指令重排序優(yōu)化。同時需要注意的是,volatile對于單個的共享變量的讀/寫具有原子性,但是像num++這種復合操作,volatile無法保證其原子性,當然文中也提出了解決方案,就是使用并發(fā)包中的原子操作類,通過循環(huán)CAS地方式來保證num++操作的原子性。關于原子操作類,會在后續(xù)的文章進行介紹。大家敬請關注腳本之家網站!

相關文章

  • Spring?boot整合ELK詳細過程

    Spring?boot整合ELK詳細過程

    ELK是由Elasticsearch、Logstash和Kibana三個開源軟件組成的技術堆棧,主要用于數據的存儲、處理和可視化,本文給大家介紹Spring?boot整合ELK詳細過程,感興趣的朋友一起看看吧
    2024-01-01
  • Java異常處理運行時異常(RuntimeException)詳解及實例

    Java異常處理運行時異常(RuntimeException)詳解及實例

    這篇文章主要介紹了 Java異常處理運行時異常(RuntimeException)詳解及實例的相關資料,需要的朋友可以參考下http://time.qq.com/?pgv_ref=aiotime
    2017-05-05
  • Java實現的猜數字游戲示例

    Java實現的猜數字游戲示例

    這篇文章主要介紹了Java實現的猜數字游戲,涉及Java數學運算與判斷相關操作技巧,需要的朋友可以參考下
    2018-06-06
  • SpringMVC?Restful風格與中文亂碼問題解決方案介紹

    SpringMVC?Restful風格與中文亂碼問題解決方案介紹

    Restful就是一個資源定位及資源操作的風格,不是標準也不是協(xié)議,只是一種風格,是對http協(xié)議的詮釋,下面這篇文章主要給大家介紹了關于SpringMVC對Restful風格支持的相關資料,需要的朋友可以參考下
    2022-10-10
  • 探究springboot中的TomcatMetricsBinder

    探究springboot中的TomcatMetricsBinder

    springboot的TomcatMetricsBinder主要是接收ApplicationStartedEvent然后創(chuàng)建TomcatMetrics執(zhí)行bindTo進行注冊,TomcatMetrics主要注冊了globalRequest、servlet、cache、threadPool、session相關的指標,本文給大家介紹的非常詳細,需要的朋友參考下吧
    2023-11-11
  • Java爬蟲(Jsoup與WebDriver)的使用

    Java爬蟲(Jsoup與WebDriver)的使用

    這篇文章主要介紹了Java爬蟲(Jsoup與WebDriver)的使用,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-12-12
  • SpringSecurity登錄使用JSON格式數據的方法

    SpringSecurity登錄使用JSON格式數據的方法

    這篇文章主要介紹了SpringSecurity登錄使用JSON格式數據的方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2019-02-02
  • Java Web開發(fā)防止多用戶重復登錄的完美解決方案

    Java Web開發(fā)防止多用戶重復登錄的完美解決方案

    在web項目開發(fā)中,很多情況下都可以讓同一個賬號信息在不同的登錄入口登錄很多次,這樣子做的不是很完善。一般解決這種情況有兩種解決方案,小編呢主要以第二種方式給大家介紹具體的實現方法,對java web 防止多用戶重復登錄的解決方案感興趣的朋友一起看看吧
    2016-11-11
  • Java編程一道多線程問題實例代碼

    Java編程一道多線程問題實例代碼

    這篇文章主要介紹了Java編程一道多線程問題實例代碼,分享了相關代碼示例,小編覺得還是挺不錯的,具有一定借鑒價值,需要的朋友可以參考下
    2018-02-02
  • Maven構建生命周期詳細介紹

    Maven構建生命周期詳細介紹

    這篇文章主要介紹了Maven構建生命周期詳細介紹,小編覺得還是挺不錯的,這里分享給大家,需要的朋友可以參考下。
    2017-11-11

最新評論