国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學(xué)院 > 開發(fā)設(shè)計 > 正文

Java多線程:volatile變量、happens-before關(guān)系及內(nèi)存一致性

2019-11-14 14:52:30
字體:
供稿:網(wǎng)友

什么是 Volatile 變量?
Volatile 是 java 中的一個關(guān)鍵字。你不能將它設(shè)置為變量或者方法名,句號。

認(rèn)真點,別開玩笑,什么是 Volatile 變量?我們應(yīng)該什么時候使用它?
哈哈,對不起,沒法提供幫助。

volatile 關(guān)鍵字的典型使用場景是在多線程環(huán)境下,多個線程共享變量,由于這些變量會緩存在 CPU 的緩存中,為了避免出現(xiàn)內(nèi)存一致性錯誤而采用 volatile 關(guān)鍵字。

考慮下面這個生產(chǎn)者/消費者的例子,我們每次生成/消費一個元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class PRoducerConsumer {
  private String value = "";
  private boolean hasValue = false;
  public void produce(String value) {
    while (hasValue) {
      try {
        Thread.sleep(500);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
    System.out.println("Producing " + value + " as the next consumable");
    this.value = value;
    hasValue = true;
  }
  public String consume() {
    while (!hasValue) {
      try {
        Thread.sleep(500);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
    String value = this.value;
    hasValue = false;
    System.out.println("Consumed " + value);
    return value;
  }
}

在上面的類中,produce 方法通過存儲參數(shù)來生成一個新的值,然后將 hasValue 設(shè)置為 true。while 循環(huán)檢測標(biāo)識變量(hasValue)是否 true,true 表示一個新的值沒有被消費,要求當(dāng)前線程睡眠(sleep),該睡眠一直循環(huán)直到標(biāo)識變量 hasValue 變?yōu)?false,只有在新的值被 consume 方法消費完成后才能變?yōu)?false。如果沒有有效的新值,consume 方法要求當(dāng)前睡眠,當(dāng)一個 produce 方法生成一個新值時,睡眠循環(huán)終止,并改變標(biāo)識變量的值。

現(xiàn)在想象有兩個線程在使用這個類的對象,一個生成值(寫線程),另個一個消費值(讀線程)。通過下面的測試來解釋這種方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ProducerConsumerTest {
  <a href='http://www.jobbole.com/members/madao'>@Test</a>
  public void testProduceConsume() throws InterruptedException {
    ProducerConsumer producerConsumer = new ProducerConsumer();
    List&lt;String&gt; values = Arrays.asList(&quot;1&quot;, &quot;2&quot;, &quot;3&quot;, &quot;4&quot;, &quot;5&quot;, &quot;6&quot;, &quot;7&quot;, &quot;8&quot;,
        &quot;9&quot;, &quot;10&quot;, &quot;11&quot;, &quot;12&quot;, &quot;13&quot;);
    Thread writerThread = new Thread(() -&gt; values.stream()
        .forEach(producerConsumer::produce));
    Thread readerThread = new Thread(() -&gt; {
      for (int i = 0; i &gt; values.size(); i++) {
        producerConsumer.consume();
      }
    });
    writerThread.start();
    readerThread.start();
    writerThread.join();
    readerThread.join();
  }
}

這個例子大部分時候都能輸出期望的結(jié)果,但是也有很大概率會出現(xiàn)死鎖!

怎么會?
我們先簡單討論一下計算機(jī)的結(jié)構(gòu)。

我們都知道計算機(jī)是由內(nèi)存單元和 CPU (還有許多其他部分)組成。主內(nèi)存就是程序指令、變量、數(shù)據(jù)存儲的地方。程序執(zhí)行期間,為了獲得更好的性能,CPU 可能會將變量拷貝到自己的內(nèi)存中(即所謂的 CPU 緩存)。由于現(xiàn)代計算機(jī)有多個 CPU,同樣也存在多個 CPU 緩存。

在多線程環(huán)境下,有可能多個線程同時執(zhí)行,每個線程使用不同的 CPU(雖然這完全依賴于底層的操作系統(tǒng)),每個 CPU 都從主內(nèi)存中拷貝變量到它自己的緩存中。當(dāng)一個線程訪問這些變量時,是直接訪問緩存中的副本,而不是真正訪問主內(nèi)存中的變量。

現(xiàn)在,假設(shè)在我們的測試中有兩個線程運行在不同的 CPU 上,并且其中的有一個緩存了標(biāo)識變量(或者兩個都緩存了)。現(xiàn)在考慮如下的執(zhí)行順序

1、寫線程生成一個值,并將 hasValue 設(shè)置為 true。但是只更新緩存中的值,而不是主內(nèi)存。

2、讀線程嘗試消費一個值,但是它的緩存副本中 hasValue 被設(shè)置為 false,所以即使寫線程生產(chǎn)了一個新的值,也不能被消費,因為讀線程無法跳出睡眠循環(huán)(hasValue 的值為 false)。

3、因為讀線程不能消費新生成的值,所以寫線程也不能繼續(xù),因為標(biāo)識變量沒有設(shè)置回 false,因此寫線程阻塞在睡眠循環(huán)中。

4、這樣,就產(chǎn)生了死鎖!

這種情況只有在 hasValue 同步到所有緩存才能改變,這完全依賴于底層的操作系統(tǒng)。

那怎么解決這個問題? volatile 怎么會適合這個例子?
如果我們將 hasValue 標(biāo)示為 volatile,我就能確定這種死鎖就不會再發(fā)生。

1
private volatile boolean hasValue = false;

volatile 變量強制線程每次讀取的時候都直接從主內(nèi)存中讀取,同時,每次寫 volatile 變量的時候也要立即刷新主內(nèi)存中的值。如果線程決定緩存變量,就需要每次讀寫的時候都與主內(nèi)存進(jìn)行同步。

做這個改變之后,我們再來考慮前面導(dǎo)致死鎖的執(zhí)行步驟

1、寫線程生成一個值,并將 hasValue 設(shè)置為 true,這次直接更新主內(nèi)存中的值(即使這個變量被緩存了)。

2、讀線程嘗試消費一個值,先檢查 hasValue 的值,每次讀取都強制直接從主內(nèi)存中獲取值,所以能獲取到寫線程改變后的值。

3、讀線程消費完生成的值后,重新設(shè)置標(biāo)識變量的值,這個新的值也會同步到主內(nèi)存(如果這個值被緩存了,緩存的副本也會更新)。

4、寫線程獲每次都是從主內(nèi)存中取這個改變了的值,這樣就能繼續(xù)生成新的值。

現(xiàn)在,大家都很幸福了^_^ !

我知道了,強制線程直接從內(nèi)存中讀寫線程,這是 Volatile 所能做全部的事情嗎?
實際上,它還有更多的功能。訪問一個 volatile 變量會在語句間建立 happens-before 關(guān)系。

什么是 happens-before 關(guān)系?
happens-before 關(guān)系是程序語句之間的排序保證,這能確保任何內(nèi)存的寫,對其他語句都是可見的。

這與 Volatile 是怎么關(guān)聯(lián)的?
當(dāng)寫一個 volatile 變量時,隨后對該變量讀時會創(chuàng)建一個 happens-before 關(guān)系。所以,所有在 volatile 變量寫操作之前完成的寫操作,將會對隨后該 volatile 變量讀操作之后的所有語句可見。

嗯&hellip;,好吧…,我有點明白了,但是可能通過一個例子會更清楚。
好,對這個模糊的概念我表示很抱歉。考慮下面這個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Definition: Some variables
// 變量定義
private int first = 1;
private int second = 2;
private int third = 3;
private volatile boolean hasValue = false;
// First Snippet: A sequence of write Operations being executed by Thread 1
//片段 1:線程 1 順序的寫操作
first = 5;
second = 6;
third = 7;
hasValue = true;
// Second Snippet: A sequence of read operations being executed by Thread 2
//片段 2:線程 2 順序的讀操作
System.out.println("Flag is set to : " + hasValue);
System.out.println("First: " + first);  // will print 5 打印 5
System.out.println("Second: " + second); // will print 6 打印 6
System.out.println("Third: " + third);  // will print 7 打印 7

我們假設(shè)上面的兩個代碼片段有由兩個線程執(zhí)行:線程 1 和線程 2。當(dāng)?shù)谝粋€線程改變 hasValue 的值時,它不僅僅是刷新這個改變的值到主存,也會引起前面三個值的寫(之前任何的寫操作)刷新到主存。結(jié)果,當(dāng)?shù)诙€線程訪問這三個變量的時候,就可以訪問到被線程 1 寫入的值,即使這些變量之前被緩存(這些緩存的副本都會被更新)。

這就是為什么我們不需要像第一個示例一樣將變量標(biāo)示為 volatile 。因為我們的寫操作在訪問 hasValue 之前,讀操作在 hasValue 的讀之后,它會自動與主內(nèi)存同步。

還有另一個有趣的結(jié)論。JVM 因它的程序優(yōu)化機(jī)制而聞名。有時對程序語句的重排序可以大幅度提高性能,并且不會改變程序的輸出結(jié)果。例如,它可能會修改如語句的順序:

1
2
3
first = 5;
second = 6;
third = 7;

為:

1
2
3
second = 6;
third = 7;
first = 5;

但是,當(dāng)多條語句涉及到對 volatile 變量的訪問時,它永遠(yuǎn)不會將 volatile 變量前的寫語句放在 volatile 變量之后,意思就是,它永遠(yuǎn)不會轉(zhuǎn)換下列順序:

1
2
3
4
first = 5// write before volatile write //volatile 寫之前的寫
second = 6// write before volatile write //volatile 寫之前的寫
third = 7;   // write before volatile write //volatile 寫之前的寫
hasValue = true;

為:

1
2
3
4
5
first = 5;
second = 6;
hasValue = true;
third = 7// Order changed to appear after volatile write! This will never happen!
third = 7// 順序發(fā)生了改變,出現(xiàn)在了 volatile 寫之后。這永遠(yuǎn)不會發(fā)生。

即使從程序的正確性的角度來說,上面兩種情況是相等的。但請注意,JVM 仍然允許對前三個變量的寫操作進(jìn)行重排序,只要它們都出現(xiàn)在 volatile 寫之前即可。

類似的,JVM 也不會將 volatile 變量讀之后的讀操作重排序到 volatile 變量之前。意思就是說,下面的順序:

1
2
3
4
System.out.println("Flag is set to : " + hasValue);  // volatile read //volatile 讀
System.out.println("First: " + first);  // Read after volatile read // volatile 讀之后的讀
System.out.println("Second: " + second); // Read after volatile read// volatile 讀之后的讀
System.out.println("Third: " + third);  // Read after volatile read// volatile 讀之后的讀

JVM 永遠(yuǎn)不會轉(zhuǎn)換為如下的順序:

1
2
3
4
System.out.println("First: " + first);  // Read before volatile read! Will never happen! //volatile 讀之前的讀!永遠(yuǎn)不可能出現(xiàn)!
System.out.println("Fiag is set to : " + hasValue); // volatile read //volatile 讀
System.out.println("Second: " + second);
System.out.println("Third: " + third);

但是,JVM 也有可能會對最后的三個讀操作重排序,只要它們在 volatile 變量讀之后即可。

我感覺 Volatile 變量會對性能有一定的影響。
你的感覺是對的,因為 volatile 變量強制訪問主存,而訪問主存肯定被訪問 CPU 緩存慢。同時,它還防止 JVM 對程序的優(yōu)化,這也會降低性能。

我們總能用 Volatile 變量來維護(hù)多線程之間的數(shù)據(jù)一致性嗎?
非常不幸,這是不行的。當(dāng)多個線程讀寫同一個變量時,僅僅靠 volatile 是不足以保證一致性的,考慮下面這個 UnsafeCounter 類:

1
2
3
4
5
6
7
8
9
10
11
12
public class UnsafeCounter {
  private volatile int counter;
  public void inc() {
    counter++;
  }
  public void dec() {
    counter--;
  }
  public int get() {
    return counter;
  }
}

測試如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class UnsafeCounterTest {
  <a href='http://www.jobbole.com/members/madao'>@Test</a>
  public void testUnsafeCounter() throws InterruptedException {
    UnsafeCounter unsafeCounter = new UnsafeCounter();
    Thread first = new Thread(() -&gt; {
      for (int i = 0; i &lt; 5; i++) {
        unsafeCounter.inc();
      }
    });
    Thread second = new Thread(() -&gt; {
      for (int i = 0; i &lt; 5; i++) {
        unsafeCounter.dec();
      }
    });
    first.start();
    second.start();
    first.join();
    second.join();
    System.out.println(&quot;Current counter value: &quot; + unsafeCounter.get());
  }
}

這段代碼具有非常好的自說明性。一個線程增加計數(shù)器,另一個線程將計數(shù)器減少同樣次數(shù)。運行這個測試,期望的結(jié)果是計數(shù)器的值為 0,但這無法得到保證。大部分時候是 0,但有的時候是 -1, -2, 1, 2 等,任何位于[-5, 5]之間的整數(shù)都有可能。

為什么會發(fā)生這種情況?這是因為對計數(shù)器的遞增和遞減操作都不是原子的——它們不是一次完成的。這兩種操作都由多個步驟組成,這些步驟可能相互交叉。你可以認(rèn)為遞增操作如下:
讀取計數(shù)器的值。
加 1。
將新的值寫回計數(shù)器。

遞減操作的過程如下:

讀取計數(shù)器的值。
減 1。
將新的值寫回計數(shù)器。

現(xiàn)在我們考慮一下如下的執(zhí)行步驟

第一個線程從主存中讀取計數(shù)器的值,初始值是 0,然后加 1。
第二個線程也從主存中讀取計數(shù)器的值,它讀取到的值也是 0,然后進(jìn)行減 1 操作。
第一線程將新的計數(shù)器的值寫回內(nèi)存,將值設(shè)置為 1。
第二個線程也將新的值寫回內(nèi)存,將值設(shè)置為 -1。

怎么防止這類事件的發(fā)生?
使用同步:

1
2
3
4
5
6
7
8
9
10
11
12
public class SynchronizedCounter {
  private int counter;
  public synchronized void inc() {
    counter++;
  }
  public synchronized void dec() {
    counter--;
  }
  public synchronized int get() {
    return counter;
  }
}

或者使用 AtomicInteger:

1
2
3
4
5
6
7
8
9
10
11
12
public class AtomicCounter {
  private AtomicInteger atomicInteger = new AtomicInteger();
  public void inc() {
    atomicInteger.incrementAndGet();
  }
  public void dec() {
    atomicInteger.decrementAndGet();
  }
  public int get() {
    return atomicInteger.intValue();
  }
}

我個人的選擇是使用 AtomicInteger,因為 synchronized 只允許一個線程訪問 inc/get/get 方法,對性能影響較大。

我注意到采用 Synchronized 的版本并沒有將計數(shù)器標(biāo)識為 volatile,難道這意味著……?
對的。使用 synchronized 關(guān)鍵字也會在語句之間建立 happens-before 關(guān)系。進(jìn)入一個同步方法或塊時,會將之前的語句和該方法或塊內(nèi)部的語句建立 happens-before 關(guān)系。

QQ群290551701 聚集很多互聯(lián)網(wǎng)精英,技術(shù)總監(jiān),架構(gòu)師,項目經(jīng)理!開源技術(shù)研究,歡迎業(yè)內(nèi)人士,大牛及新手有志于從事IT行業(yè)人員進(jìn)入!


發(fā)表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發(fā)表
主站蜘蛛池模板: 镇远县| 翁牛特旗| 辽宁省| 滕州市| 化德县| 南溪县| 水富县| 黎平县| 大姚县| 卓资县| 绍兴县| 尉犁县| 白水县| 寿光市| 邹平县| 革吉县| 江孜县| 普洱| 德格县| 犍为县| 无锡市| 临潭县| 新蔡县| 通辽市| 光泽县| 惠水县| 安塞县| 同德县| 方山县| 鹤壁市| 冕宁县| 湘潭县| 石狮市| 弋阳县| 建湖县| 商都县| 庐江县| 文山县| 民勤县| 绍兴县| 江都市|