中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

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

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

如何解決volatile和happens-before的關系與內存一致性錯誤

發布時間:2021-07-13 16:10:32 來源:億速云 閱讀:159 作者:小新 欄目:編程語言

這篇文章主要介紹了如何解決volatile和happens-before的關系與內存一致性錯誤,具有一定借鑒價值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。

volatile變量

volatile是Java的關鍵詞,我們可以用它來修飾變量或者方法。

為什么要使用volatile
volatile的典型用法是,當多個線程共享變量,且我們要避免由于內存緩沖變量導致的內存一致性(Memory Consistency Errors)錯誤時。

考慮以下的生產者消費者例子,在一個時刻我們生產或消費一個單位。

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方法產生一個新的值,并保存在value變量中,并且將hasValue標志位置為true。while循環檢查hasValue是否為true,為true則標志產生的數據還沒有被消費,如果為true,則休眠當前線程。當hasValue置為false的時候,休眠循環才會停止,也就是將數據被consume方法消費后。如果沒有可用的數據,cosume方法會休眠。當produce方法產生一個新的數據后,consume會結束休眠,消費該數據,并清除hasValue標志位。

現在設想兩個線程使用該類的同一個對象——一個用來產生數據(write線程),另一個用來消耗數據(read線程)。實例代碼如下,

public class ProducerConsumerTest {
 @Test
 public void testProduceConsume() throws InterruptedException {
 ProducerConsumer producerConsumer = new ProducerConsumer();
 List<String> values = Arrays.asList("1", "2", "3", "4", "5", "6", "7", "8",
 "9", "10", "11", "12", "13");
 Thread writerThread = new Thread(() -> values.stream()
 .forEach(producerConsumer::produce));
 Thread readerThread = new Thread(() -> {
 for (int i = 0; i > values.size(); i++) {
 producerConsumer.consume();
 }
 });
 writerThread.start();
 readerThread.start();
 writerThread.join();
 readerThread.join();
 }
}

在大多數情況下,該例子會輸出預期的結果,但是也有很大的可能進入死鎖狀態!

為什么會發生該現象?

首先我們介紹一點計算機架構的知識。

我們知道計算機包括了CPU和內存單元(還有其他組件)。程序指令和變量處在的內存成為主內存;在程序執行期間,為了更好的性能,CPU可能會在其內部內存(也就是CPU緩沖)中存放變量的拷貝。由于現在計算機包括了不止一個CPU,所以同時也包括了多個CPU緩沖。

在多線程環境中,多個線程有可能在同一個時間運行,每個在不同的CPU(由底層OS決定),并且他們可能從主內存中復制變量到對應的CPU緩沖中。當線程訪問這些變量時,其訪問的是這些緩沖的變量,并不是位于主內存的實際變量。

現在我們假設上個例子中的兩個線程運行在兩個不同的CPU上,并且hasValue變量被緩沖在其中一個CPU上(或者兩個)。考慮以下的執行序列:

1.writer線程產生一個數據,并將hasValue設置為true。然而,這個改變只是體現在CPU緩沖上,而不是主內存。
2.reader線程準備消耗一個數據,但是其CPU緩沖的hasValue為false。所以即使writer線程產生了一個數據,reader線程也不能消耗該數據。
3.由于reader線程無法消費新產生的數據,writer線程也不能繼續產生新的數據(由于hasValue為true),因此writer會休眠。
4.然后就出現了死鎖!

當hasValue值在所有的緩沖中都同步(基于底層OS),該情形就會改變。

解決方案?volatile如何適用該例子?

如果我們將hasValue設置為volatile,那么我們可以保證這種類型的死鎖不會出現。

private volatile boolean hasValue = false;
將一個變量設置為volatile后,線程就會直接從主內存中讀取該變量的值,并且該變量的寫入會立即刷新到主內存中。如果一個線程緩沖了該變量,那么每次讀和寫操作都會和主內存同步。

這個修改后,考慮上面那個可能會導致死鎖的步驟:

1.writer產生了一個新的數據,并將hasValue設置為true。該更新會直接反映在主內存中(即使該線程使用了緩存)。
2.reader線程嘗試消費一個變量,并檢查hasValue的值。該變量的每次讀都會直接從主內存獲得,所以它能獲得到writer線程導致的改變。
3.reader線程消費該變量并清楚hasValue標志位。該變量會刷新到主內存中(如果被緩存,則緩存的變量也會刷新)。
4.由于reader線程每次都操作的主內存,所以writer線程能看到reader導致的改變。其會繼續產生新的數據。

volatile與happens-before關系

訪問volatile變量在語句間建立了happens-before關系。當寫入一個volatile變量時,它與之后的該變量的讀操作建立了happens-before關系。那么什么是happens-before關系呢?可以參考筆者之前的博客[Java并發編程番外篇(二)happens-before關系],簡單來說,就是保證一個語句的影響會被另一個語句看到(https://www.jb51.net/article/161649.htm)。

考慮以下的例子,

// 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
first = 5;
second = 6;
third = 7;
hasValue = true;
// Second Snippet: A sequence of read operations being executed by Thread 2
System.out.println("Flag is set to : " + hasValue);
System.out.println("First: " + first); // will print 5
System.out.println("Second: " + second); // will print 6
System.out.println("Third: " + third); // will print 7

我們假設兩面的兩個片段運行在兩個線程——線程1和線程2. 當線程1修改hasValue值后,不僅僅hasValue的值會直接寫入到主內存,前面的三個寫操作也會寫入主內存(和之前的其他寫操作)。因此,當線程2訪問這三個變量時,它會看到線程1對這些變量進行的修改,即使他們會緩存(這些緩存也會被更新)。

這也正是在第一個例子中,我們沒有將value變量設置為volatile的原因。這是由于訪問hasValue之前其他變量的寫操作,和讀hashValue之后其他變量的讀操作,會自動和主內存同步。

這是另外一個有趣的序列。JVM以它的程序優化著名。有時候,在不影響輸出的情況下,JVM會對指令進行重排序來獲得更好的性能。作為例子,它可能將該序列的代碼,

first = 5;
second = 6;
third = 7;

重排序為,

first = 5;
second = 6;
third = 7;

然而,當一個語句涉及到訪問volatile變量,那么JVM就不會將一個volatile寫操作之前的語句放到volatile寫操作之后。也就是說,它不會將以下的代碼序列,

first = 5; // write before volatile write
second = 6; // write before volatile write
third = 7; // write before volatile write
hasValue = true;

修改成,

first = 5;
second = 6;
hasValue = true;
third = 7; // Order changed to appear after volatile write! This will never happen!

即使從代碼正確性的角度來看,這兩者是相同的。注意到JVM仍然允許重排序前三條語句,只要他們位于volatile寫之前。

類似,JVM不會將位于volatile讀之后的代碼重排序到volatile讀之前。也就是說該代碼,

System.out.println("Flag is set to : " + hasValue); // volatile read
System.out.println("First: " + first); // Read after volatile read
System.out.println("Second: " + second); // Read after volatile read
System.out.println("Third: " + third); // Read after volatile read

并不會修改為,

http://System.out.println("First: " + first); // Read before volatile read! Will never happen!
System.out.println("Fiag is set to : " + hasValue); // volatile read
System.out.println("Second: " + second); 
System.out.println("Third: " + third);

然而,JVM可以將后三條語句重排序,只要他們在volatile讀之后。

volatile帶來的性能開銷

volatile強制進行主內存訪問,而主內存訪問通常比CPU緩存訪問慢。同時也阻止了JVM進行的一些程序優化,更進一步降低了性能。

能否使用volatile來保證多線程的數據一致性?

答案是不能。當多個線程訪問同一個變量時,將該變量標志為volatile并不足以保證一致性,考慮下面的UnsafeCounter類,

public class UnsafeCounter {
 private volatile int counter;
 public void inc() {
 counter++;
 }
 public void dec() {
 counter--;
 }
 public int get() {
 return counter;
 }
}

測試代碼,

public class UnsafeCounter {
 private volatile int counter;
 public void inc() {
 counter++;
 }
 public void dec() {
 counter--;
 }
 public int get() {
 return counter;
 }
}

代碼很容易讀懂。我們在一個線程中增加計數器的值,然后在另一個線程中減少計數器的值。運行這個測試,我們預期的計數器的結果是0,但是這并不能保證。大多數情況下都是0,然而,一些情況下,可能是-2,-1,1,2,甚至[-5,5]的任何數字。

為什么會發生這種情況呢?這是由于counter變量的增加和減少操作都不是原子操作——他們不是一次執行完畢的。他們都包括了多個步驟,而且兩個步驟序列有交疊。你可以認為自增這樣操作:

1.讀取counter數值
2.增加1
3.將數值寫入到counter中

同樣的,自減操作:

1.讀取counter數值
2.減少1
3.將數值寫入到counter中

現在,我們考慮以下的執行序列:

1.第一個線程從內存中讀取counter的值。其被初始化為0. 然后該線程將其自增.
2.第二個線程同時也從內存中讀取counter的值,并且該值也為0. 然后該線程對其執行自減操作。
3.第一個進程將數值寫入到內存中,即,counter的值為1.
4.第二個線程將數值寫入到內存中,即,counter的值為-1.
5.第一個線程的更新被丟失。

怎么阻止該現象呢?

1. 使用同步:

public class SynchronizedCounter {
 private int counter;
 public synchronized void inc() {
 counter++;
 }
 public synchronized void dec() {
 counter--;
 }
 public synchronized int get() {
 return counter;
 }
}

2. 或者使用AtomicInteger:

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,因為同步方法只允許一個線程訪問inc/dec/get方法,這帶來了額外的性能開銷。

使用同步方法時,我們并沒有將counter設置為volatile變量。這是因為,使用synchronized關鍵詞就建立了happens-before關系。進入一個同步方法(代碼塊),在該語句之前的代碼和方法(代碼塊)中的代碼建立了happens-before關系。

感謝你能夠認真閱讀完這篇文章,希望小編分享的“如何解決volatile和happens-before的關系與內存一致性錯誤”這篇文章對大家有幫助,同時也希望大家多多支持億速云,關注億速云行業資訊頻道,更多相關知識等著你來學習!

向AI問一下細節

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

AI

新建县| 永昌县| 隆化县| 玛曲县| 台安县| 惠安县| 金寨县| 营口市| 札达县| 莱州市| 阿瓦提县| 商洛市| 五河县| 芒康县| 徐闻县| 五常市| 噶尔县| 寿阳县| 监利县| 星座| 广丰县| 楚雄市| 长武县| 山西省| 灵石县| 长治市| 论坛| 呈贡县| 盐亭县| 龙门县| 金溪县| 罗源县| 嫩江县| 澳门| 二连浩特市| 宁波市| 中牟县| 临清市| 通州市| 高邑县| 蒲城县|