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

Java 并發(fā)編程:volatile的使用及其原理解析

 更新時(shí)間:2016年05月17日 10:28:40   投稿:jingxian  
下面小編就為大家?guī)?lái)一篇Java 并發(fā)編程:volatile的使用及其原理解析。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧

Java并發(fā)編程系列【未完】:

•Java 并發(fā)編程:核心理論

•Java并發(fā)編程:Synchronized及其實(shí)現(xiàn)原理

•Java并發(fā)編程:Synchronized底層優(yōu)化(輕量級(jí)鎖、偏向鎖)

•Java 并發(fā)編程:線程間的協(xié)作(wait/notify/sleep/yield/join)

•Java 并發(fā)編程:volatile的使用及其原理

一、volatile的作用

在《Java并發(fā)編程:核心理論》一文中,我們已經(jīng)提到過(guò)可見(jiàn)性、有序性及原子性問(wèn)題,通常情況下我們可以通過(guò)Synchronized關(guān)鍵字來(lái)解決這些個(gè)問(wèn)題,不過(guò)如果對(duì)Synchronized原理有了解的話,應(yīng)該知道Synchronized是一個(gè)比較重量級(jí)的操作,對(duì)系統(tǒng)的性能有比較大的影響,所以,如果有其他解決方案,我們通常都避免使用Synchronized來(lái)解決問(wèn)題。而volatile關(guān)鍵字就是Java中提供的另一種解決可見(jiàn)性和有序性問(wèn)題的方案。對(duì)于原子性,需要強(qiáng)調(diào)一點(diǎn),也是大家容易誤解的一點(diǎn):對(duì)volatile變量的單次讀/寫(xiě)操作可以保證原子性的,如long和double類型變量,但是并不能保證i++這種操作的原子性,因?yàn)楸举|(zhì)上i++是讀、寫(xiě)兩次操作。

二、volatile的使用

關(guān)于volatile的使用,我們可以通過(guò)幾個(gè)例子來(lái)說(shuō)明其使用方式和場(chǎng)景。

1、防止重排序

我們從一個(gè)最經(jīng)典的例子來(lái)分析重排序問(wèn)題。大家應(yīng)該都很熟悉單例模式的實(shí)現(xiàn),而在并發(fā)環(huán)境下的單例實(shí)現(xiàn)方式,我們通??梢圆捎秒p重檢查加鎖(DCL)的方式來(lái)實(shí)現(xiàn)。其源碼如下:

package com.paddx.test.concurrent;

public class Singleton {
  public static volatile Singleton singleton;

  /**
   * 構(gòu)造函數(shù)私有,禁止外部實(shí)例化
   */
  private Singleton() {};

  public static Singleton getInstance() {
    if (singleton == null) {
      synchronized (singleton) {
        if (singleton == null) {
          singleton = new Singleton();
        }
      }
    }
    return singleton;
  }
}

現(xiàn)在我們分析一下為什么要在變量singleton之間加上volatile關(guān)鍵字。要理解這個(gè)問(wèn)題,先要了解對(duì)象的構(gòu)造過(guò)程,實(shí)例化一個(gè)對(duì)象其實(shí)可以分為三個(gè)步驟:

(1)分配內(nèi)存空間。

(2)初始化對(duì)象。

(3)將內(nèi)存空間的地址賦值給對(duì)應(yīng)的引用。

但是由于操作系統(tǒng)可以對(duì)指令進(jìn)行重排序,所以上面的過(guò)程也可能會(huì)變成如下過(guò)程:

(1)分配內(nèi)存空間。

(2)將內(nèi)存空間的地址賦值給對(duì)應(yīng)的引用。

(3)初始化對(duì)象

如果是這個(gè)流程,多線程環(huán)境下就可能將一個(gè)未初始化的對(duì)象引用暴露出來(lái),從而導(dǎo)致不可預(yù)料的結(jié)果。因此,為了防止這個(gè)過(guò)程的重排序,我們需要將變量設(shè)置為volatile類型的變量。

2、實(shí)現(xiàn)可見(jiàn)性

可見(jiàn)性問(wèn)題主要指一個(gè)線程修改了共享變量值,而另一個(gè)線程卻看不到。引起可見(jiàn)性問(wèn)題的主要原因是每個(gè)線程擁有自己的一個(gè)高速緩存區(qū)——線程工作內(nèi)存。volatile關(guān)鍵字能有效的解決這個(gè)問(wèn)題,我們看下下面的例子,就可以知道其作用:

package com.paddx.test.concurrent;

public class VolatileTest {
  int a = 1;
  int b = 2;

  public void change(){
    a = 3;
    b = a;
  }

  public void print(){
    System.out.println("b="+b+";a="+a);
  }

  public static void main(String[] args) {
    while (true){
      final VolatileTest test = new VolatileTest();
      new Thread(new Runnable() {
        @Override
        public void run() {
          try {
            Thread.sleep(10);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          test.change();
        }
      }).start();

      new Thread(new Runnable() {
        @Override
        public void run() {
          try {
            Thread.sleep(10);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          test.print();
        }
      }).start();

    }
  }
}

直觀上說(shuō),這段代碼的結(jié)果只可能有兩種:b=3;a=3 或 b=2;a=1。不過(guò)運(yùn)行上面的代碼(可能時(shí)間上要長(zhǎng)一點(diǎn)),你會(huì)發(fā)現(xiàn)除了上兩種結(jié)果之外,還出現(xiàn)了第三種結(jié)果:

...... 
b=2;a=1
b=2;a=1
b=3;a=3
b=3;a=3
b=3;a=1
b=3;a=3
b=2;a=1
b=3;a=3
b=3;a=3
......

為什么會(huì)出現(xiàn)b=3;a=1這種結(jié)果呢?正常情況下,如果先執(zhí)行change方法,再執(zhí)行print方法,輸出結(jié)果應(yīng)該為b=3;a=3。相反,如果先執(zhí)行的print方法,再執(zhí)行change方法,結(jié)果應(yīng)該是 b=2;a=1。那b=3;a=1的結(jié)果是怎么出來(lái)的?原因就是第一個(gè)線程將值a=3修改后,但是對(duì)第二個(gè)線程是不可見(jiàn)的,所以才出現(xiàn)這一結(jié)果。如果將a和b都改成volatile類型的變量再執(zhí)行,則再也不會(huì)出現(xiàn)b=3;a=1的結(jié)果了。

3、保證原子性

關(guān)于原子性的問(wèn)題,上面已經(jīng)解釋過(guò)。volatile只能保證對(duì)單次讀/寫(xiě)的原子性。這個(gè)問(wèn)題可以看下JLS中的描述:

17.7 Non-Atomic Treatment of double and long 
For the purposes of the Java programming language memory model, a single write to a non-volatile long or double value is treated as two separate writes: one to each 32-bit half. This can result in a situation where a thread sees the first 32 bits of a 64-bit value from one write, and the second 32 bits from another write.

Writes and reads of volatile long and double values are always atomic. 

Writes to and reads of references are always atomic, regardless of whether they are implemented as 32-bit or 64-bit values. 

Some implementations may find it convenient to divide a single write action on a 64-bit long or double value into two write actions on adjacent 32-bit values. For efficiency's sake, this behavior is implementation-specific; an implementation of the Java Virtual Machine is free to perform writes to long and double values atomically or in two parts. 

Implementations of the Java Virtual Machine are encouraged to avoid splitting 64-bit values where possible. Programmers are encouraged to declare shared 64-bit values as volatile or synchronize their programs correctly to avoid possible complications.

這段話的內(nèi)容跟我前面的描述內(nèi)容大致類似。因?yàn)閘ong和double兩種數(shù)據(jù)類型的操作可分為高32位和低32位兩部分,因此普通的long或double類型讀/寫(xiě)可能不是原子的。因此,鼓勵(lì)大家將共享的long和double變量設(shè)置為volatile類型,這樣能保證任何情況下對(duì)long和double的單次讀/寫(xiě)操作都具有原子性。

關(guān)于volatile變量對(duì)原子性保證,有一個(gè)問(wèn)題容易被誤解?,F(xiàn)在我們就通過(guò)下列程序來(lái)演示一下這個(gè)問(wèn)題:

package com.paddx.test.concurrent;

public class VolatileTest01 {
  volatile int i;

  public void addI(){
    i++;
  }

  public static void main(String[] args) throws InterruptedException {
    final VolatileTest01 test01 = new VolatileTest01();
    for (int n = 0; n < 1000; n++) {
      new Thread(new Runnable() {
        @Override
        public void run() {
          try {
            Thread.sleep(10);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          test01.addI();
        }
      }).start();
    }

    Thread.sleep(10000);//等待10秒,保證上面程序執(zhí)行完成

    System.out.println(test01.i);
  }
}

大家可能會(huì)誤認(rèn)為對(duì)變量i加上關(guān)鍵字volatile后,這段程序就是線程安全的。大家可以嘗試運(yùn)行上面的程序。下面是我本地運(yùn)行的結(jié)果:

可能每個(gè)人運(yùn)行的結(jié)果不相同。不過(guò)應(yīng)該能看出,volatile是無(wú)法保證原子性的(否則結(jié)果應(yīng)該是1000)。原因也很簡(jiǎn)單,i++其實(shí)是一個(gè)復(fù)合操作,包括三步驟:

(1)讀取i的值。

(2)對(duì)i加1。

(3)將i的值寫(xiě)回內(nèi)存。

volatile是無(wú)法保證這三個(gè)操作是具有原子性的,我們可以通過(guò)AtomicInteger或者Synchronized來(lái)保證+1操作的原子性。

注:上面幾段代碼中多處執(zhí)行了Thread.sleep()方法,目的是為了增加并發(fā)問(wèn)題的產(chǎn)生幾率,無(wú)其他作用。

三、volatile的原理

通過(guò)上面的例子,我們基本應(yīng)該知道了volatile是什么以及怎么使用。現(xiàn)在我們?cè)賮?lái)看看volatile的底層是怎么實(shí)現(xiàn)的。

1、可見(jiàn)性實(shí)現(xiàn):

在前文中已經(jīng)提及過(guò),線程本身并不直接與主內(nèi)存進(jìn)行數(shù)據(jù)的交互,而是通過(guò)線程的工作內(nèi)存來(lái)完成相應(yīng)的操作。這也是導(dǎo)致線程間數(shù)據(jù)不可見(jiàn)的本質(zhì)原因。因此要實(shí)現(xiàn)volatile變量的可見(jiàn)性,直接從這方面入手即可。對(duì)volatile變量的寫(xiě)操作與普通變量的主要區(qū)別有兩點(diǎn):

(1)修改volatile變量時(shí)會(huì)強(qiáng)制將修改后的值刷新的主內(nèi)存中。

(2)修改volatile變量后會(huì)導(dǎo)致其他線程工作內(nèi)存中對(duì)應(yīng)的變量值失效。因此,再讀取該變量值的時(shí)候就需要重新從讀取主內(nèi)存中的值。

通過(guò)這兩個(gè)操作,就可以解決volatile變量的可見(jiàn)性問(wèn)題。

2、有序性實(shí)現(xiàn):

 在解釋這個(gè)問(wèn)題前,我們先來(lái)了解一下Java中的happen-before規(guī)則,JSR 133中對(duì)Happen-before的定義如下:

Two actions can be ordered by a happens-before relationship.If one action happens before another, then the first is visible to and ordered before the second.

通俗一點(diǎn)說(shuō)就是如果a happen-before b,則a所做的任何操作對(duì)b是可見(jiàn)的。(這一點(diǎn)大家務(wù)必記住,因?yàn)閔appen-before這個(gè)詞容易被誤解為是時(shí)間的前后)。我們?cè)賮?lái)看看JSR 133中定義了哪些happen-before規(guī)則:

• Each action in a thread happens before every subsequent action in that thread. 
• An unlock on a monitor happens before every subsequent lock on that monitor. 
• A write to a volatile field happens before every subsequent read of that volatile. 
• A call to start() on a thread happens before any actions in the started thread. 
• All actions in a thread happen before any other thread successfully returns from a join() on that thread. 
• If an action a happens before an action b, and b happens before an action c, then a happens before c.

翻譯過(guò)來(lái)為:

•同一個(gè)線程中的,前面的操作 happen-before 后續(xù)的操作。(即單線程內(nèi)按代碼順序執(zhí)行。但是,在不影響在單線程環(huán)境執(zhí)行結(jié)果的前提下,編譯器和處理器可以進(jìn)行重排序,這是合法的。換句話說(shuō),這一是規(guī)則無(wú)法保證編譯重排和指令重排)。

•監(jiān)視器上的解鎖操作 happen-before 其后續(xù)的加鎖操作。(Synchronized 規(guī)則)

•對(duì)volatile變量的寫(xiě)操作 happen-before 后續(xù)的讀操作。(volatile 規(guī)則)

•線程的start() 方法 happen-before 該線程所有的后續(xù)操作。(線程啟動(dòng)規(guī)則)

•線程所有的操作 happen-before 其他線程在該線程上調(diào)用 join 返回成功后的操作。

•如果 a happen-before b,b happen-before c,則a happen-before c(傳遞性)。

這里我們主要看下第三條:volatile變量的保證有序性的規(guī)則?!禞ava并發(fā)編程:核心理論》一文中提到過(guò)重排序分為編譯器重排序和處理器重排序。為了實(shí)現(xiàn)volatile內(nèi)存語(yǔ)義,JMM會(huì)對(duì)volatile變量限制這兩種類型的重排序。下面是JMM針對(duì)volatile變量所規(guī)定的重排序規(guī)則表:

Can Reorder 2nd operation
1st operation Normal Load
Normal Store
Volatile Load Volatile Store
Normal Load
Normal Store


No
Volatile Load No No No
Volatile store
No No

3、內(nèi)存屏障

為了實(shí)現(xiàn)volatile可見(jiàn)性和happen-befor的語(yǔ)義。JVM底層是通過(guò)一個(gè)叫做“內(nèi)存屏障”的東西來(lái)完成。內(nèi)存屏障,也叫做內(nèi)存柵欄,是一組處理器指令,用于實(shí)現(xiàn)對(duì)內(nèi)存操作的順序限制。下面是完成上述規(guī)則所要求的內(nèi)存屏障:

Required barriers 2nd operation
1st operation Normal Load Normal Store Volatile Load Volatile Store
Normal Load


LoadStore
Normal Store


StoreStore
Volatile Load LoadLoad LoadStore LoadLoad LoadStore
Volatile Store

StoreLoad StoreStore

(1)LoadLoad 屏障

執(zhí)行順序:Load1—>Loadload—>Load2
確保Load2及后續(xù)Load指令加載數(shù)據(jù)之前能訪問(wèn)到Load1加載的數(shù)據(jù)。

(2)StoreStore 屏障

執(zhí)行順序:Store1—>StoreStore—>Store2
確保Store2以及后續(xù)Store指令執(zhí)行前,Store1操作的數(shù)據(jù)對(duì)其它處理器可見(jiàn)。

(3)LoadStore 屏障

執(zhí)行順序: Load1—>LoadStore—>Store2
確保Store2和后續(xù)Store指令執(zhí)行前,可以訪問(wèn)到Load1加載的數(shù)據(jù)。

(4)StoreLoad 屏障

執(zhí)行順序: Store1—> StoreLoad—>Load2
確保Load2和后續(xù)的Load指令讀取之前,Store1的數(shù)據(jù)對(duì)其他處理器是可見(jiàn)的。

最后我可以通過(guò)一個(gè)實(shí)例來(lái)說(shuō)明一下JVM中是如何插入內(nèi)存屏障的:

package com.paddx.test.concurrent;

public class MemoryBarrier {
  int a, b;
  volatile int v, u;

  void f() {
    int i, j;

    i = a;
    j = b;
    i = v;
    //LoadLoad
    j = u;
    //LoadStore
    a = i;
    b = j;
    //StoreStore
    v = i;
    //StoreStore
    u = j;
    //StoreLoad
    i = u;
    //LoadLoad
    //LoadStore
    j = b;
    a = i;
  }
}

四、總結(jié)

總體上來(lái)說(shuō)volatile的理解還是比較困難的,如果不是特別理解,也不用急,完全理解需要一個(gè)過(guò)程,在后續(xù)的文章中也還會(huì)多次看到volatile的使用場(chǎng)景。這里暫且對(duì)volatile的基礎(chǔ)知識(shí)和原來(lái)有一個(gè)基本的了解??傮w來(lái)說(shuō),volatile是并發(fā)編程中的一種優(yōu)化,在某些場(chǎng)景下可以代替Synchronized。但是,volatile的不能完全取代Synchronized的位置,只有在一些特殊的場(chǎng)景下,才能適用volatile??偟膩?lái)說(shuō),必須同時(shí)滿足下面兩個(gè)條件才能保證在并發(fā)環(huán)境的線程安全:

(1)對(duì)變量的寫(xiě)操作不依賴于當(dāng)前值。

(2)該變量沒(méi)有包含在具有其他變量的不變式中。

以上這篇Java 并發(fā)編程:volatile的使用及其原理解析就是小編分享給大家的全部?jī)?nèi)容了,希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • 詳談Java中instanceof和isInstance的區(qū)別

    詳談Java中instanceof和isInstance的區(qū)別

    下面小編就為大家?guī)?lái)一篇詳談Java中instanceof和isInstance的區(qū)別。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2017-01-01
  • 教你使用springboot配置多數(shù)據(jù)源

    教你使用springboot配置多數(shù)據(jù)源

    發(fā)現(xiàn)有很多小伙伴還不會(huì)用springboot配置多數(shù)據(jù)源,今天特地給大家整理了本篇文章,文中有非常詳細(xì)的圖文介紹及代碼示例,對(duì)正在學(xué)習(xí)java的小伙伴很有幫助,需要的朋友可以參考下
    2021-05-05
  • java數(shù)據(jù)結(jié)構(gòu)與算法之快速排序詳解

    java數(shù)據(jù)結(jié)構(gòu)與算法之快速排序詳解

    這篇文章主要介紹了java數(shù)據(jù)結(jié)構(gòu)與算法之快速排序,結(jié)合實(shí)例形式詳細(xì)分析了快速排序的原理、實(shí)現(xiàn)步驟、相關(guān)操作技巧與注意事項(xiàng),需要的朋友可以參考下
    2017-05-05
  • Java實(shí)現(xiàn)一個(gè)順序表的完整代碼

    Java實(shí)現(xiàn)一個(gè)順序表的完整代碼

    順序表是用一段物理地址連續(xù)的存儲(chǔ)單元依次存儲(chǔ)數(shù)據(jù)元素的線性結(jié)構(gòu),一般采用數(shù)組存儲(chǔ)。在數(shù)組上完成數(shù)據(jù)的增刪減改。順序表的底層是一個(gè)數(shù)組
    2021-04-04
  • Java日常練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(30)

    Java日常練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(30)

    下面小編就為大家?guī)?lái)一篇Java基礎(chǔ)的幾道練習(xí)題(分享)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧,希望可以幫到你
    2021-07-07
  • 通過(guò)Java 程序獲取Word中指定圖片的坐標(biāo)位置

    通過(guò)Java 程序獲取Word中指定圖片的坐標(biāo)位置

    本文介紹通過(guò)Java程序獲取Word文檔中指定圖片的坐標(biāo)位置,程序運(yùn)行環(huán)境是jdk1.8開(kāi)發(fā)環(huán)境idea,通過(guò)java程序代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧
    2021-05-05
  • 從內(nèi)存模型中了解Java final的全部細(xì)節(jié)

    從內(nèi)存模型中了解Java final的全部細(xì)節(jié)

    關(guān)于final關(guān)鍵字,它也是我們一個(gè)經(jīng)常用的關(guān)鍵字,可以修飾在類上、或者修飾在變量、方法上,以此看來(lái)定義它的一些不可變性!像我們經(jīng)常使用的String類中,它便是final來(lái)修飾的類,并且它的字符數(shù)組也是被final所修飾的。但是一些final的一些細(xì)節(jié)你真的了解過(guò)嗎
    2022-03-03
  • 使用idea開(kāi)發(fā)Servlet詳細(xì)圖文教程

    使用idea開(kāi)發(fā)Servlet詳細(xì)圖文教程

    這篇文章主要給大家介紹了關(guān)于使用idea開(kāi)發(fā)Servlet的相關(guān)資料,將idea添加servlet的過(guò)程其實(shí)非常簡(jiǎn)單,只需要按照以下幾個(gè)步驟即可完成,需要的朋友可以參考下
    2023-10-10
  • Spring Boot配置過(guò)濾器的2種方式示例

    Spring Boot配置過(guò)濾器的2種方式示例

    這篇文章主要給大家介紹了關(guān)于Spring Boot配置過(guò)濾器的2種方式,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Spring Boot具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-09-09
  • Java中的system.getProperty()的作用及使用方法

    Java中的system.getProperty()的作用及使用方法

    System.getProperty()?方法用于獲取系統(tǒng)屬性的值,該方法接受一個(gè)字符串參數(shù),表示要獲取的系統(tǒng)屬性的名稱,返回值為字符串類型,表示該屬性的值,接下來(lái)通過(guò)本文給大家介紹Java中的system.getProperty()的作用及使用方法,感興趣的朋友跟隨小編一起看看吧
    2023-05-05

最新評(píng)論