日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

java volatile 死锁_Java 多线程:volatile 变量、happens-before 关系及内存一致性

發布時間:2024/9/19 java 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 java volatile 死锁_Java 多线程:volatile 变量、happens-before 关系及内存一致性 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

原標題:Java 多線程:volatile 變量、happens-before 關系及內存一致性

來源:ImportNew - paddx

更新

請參考來自 Jean-philippe Bempel 的評論。他提到了一個真實因 JVM 優化導致死鎖的例子。我盡可能多地寫博客的原因之一是一旦自己理解錯了,可以從社區中學到很多。謝謝!

什么是 Volatile 變量?

Volatile 是 Java 中的一個關鍵字。你不能將它設置為變量或者方法名,句號。

認真點,別開玩笑,什么是 Volatile 變量?我們應該什么時候使用它?

哈哈,對不起,沒法提供幫助。

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

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

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 方法通過存儲參數來生成一個新的值,然后將 hasValue 設置為 true。while 循環檢測標識變量(hasValue)是否 true,true 表示一個新的值沒有被消費,要求當前線程睡眠(sleep),該睡眠一直循環直到標識變量 hasValue 變為 false,只有在新的值被 consume 方法消費完成后才能變為 false。如果沒有有效的新值,consume 方法要求當前睡眠,當一個 produce 方法生成一個新值時,睡眠循環終止,并改變標識變量的值。

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

public class ProducerConsumerTest {

@Test

public void testProduceConsume() throws InterruptedException {

ProducerConsumer producerConsumer = new ProducerConsumer();

List 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(雖然這完全依賴于底層的操作系統),每個 CPU 都從主內存中拷貝變量到它自己的緩存中。當一個線程訪問這些變量時,是直接訪問緩存中的副本,而不是真正訪問主內存中的變量。

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

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

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

3、因為讀線程不能消費新生成的值,所以寫線程也不能繼續,因為標識變量沒有設置回 false,因此寫線程阻塞在睡眠循環中。

4、這樣,就產生了死鎖!

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

那怎么解決這個問題? volatile 怎么會適合這個例子?

如果我們將 hasValue 標示為 volatile,我就能確定這種死鎖就不會再發生。

private volatile boolean hasValue = false;

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

做這個改變之后,我們再來考慮前面導致死鎖的執行步驟

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

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

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

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

現在,大家都很幸福了^_^ !

我知道了,強制線程直接從內存中讀寫線程,這是 Volatile 所能做全部的事情嗎?

實際上,它還有更多的功能。訪問一個 volatile 變量會在語句間建立 happens-before 關系。

什么是 happens-before 關系?

happens-before 關系是程序語句之間的排序保證,這能確保任何內存的寫,對其他語句都是可見的。

這與 Volatile 是怎么關聯的?

當寫一個 volatile 變量時,隨后對該變量讀時會創建一個 happens-before 關系。所以,所有在 volatile 變量寫操作之前完成的寫操作,將會對隨后該 volatile 變量讀操作之后的所有語句可見。

嗯…,好吧…,我有點明白了,但是可能通過一個例子會更清楚。

好,對這個模糊的概念我表示很抱歉。考慮下面這個例子:

// 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

我們假設上面的兩個代碼片段有由兩個線程執行:線程 1 和線程 2。當第一個線程改變 hasValue 的值時,它不僅僅是刷新這個改變的值到主存,也會引起前面三個值的寫(之前任何的寫操作)刷新到主存。結果,當第二個線程訪問這三個變量的時候,就可以訪問到被線程 1 寫入的值,即使這些變量之前被緩存(這些緩存的副本都會被更新)。

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

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

first = 5;

second = 6;

third = 7;

為:

second = 6;

third = 7;

first = 5;

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

first = 5; // write before volatile write //volatile 寫之前的寫

second = 6; // write before volatile write //volatile 寫之前的寫

third = 7; // write before volatile write //volatile 寫之前的寫

hasValue = true;

為:

first = 5;

second = 6;

hasValue = true;

third = 7; // Order changed to appear after volatile write! This will never happen!

third = 7; // 順序發生了改變,出現在了 volatile 寫之后。這永遠不會發生。

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

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

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 永遠不會轉換為如下的順序:

System.out.println("First: " + first); // Read before volatile read! Will never happen! //volatile 讀之前的讀!永遠不可能出現!

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 對程序的優化,這也會降低性能。

我們總能用 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 UnsafeCounterTest {

@Test

public void testUnsafeCounter() throws InterruptedException {

UnsafeCounter unsafeCounter = new UnsafeCounter();

Thread first = new Thread(() -> {

for (int i = 0; i < 5; i++) {

unsafeCounter.inc();

}

});

Thread second = new Thread(() -> {

for (int i = 0; i < 5; i++) {

unsafeCounter.dec();

}

});

first.start();

second.start();

first.join();

second.join();

System.out.println("Current counter value: " + unsafeCounter.get());

}

}

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

為什么會發生這種情況?這是因為對計數器的遞增和遞減操作都不是原子的——它們不是一次完成的。這兩種操作都由多個步驟組成,這些步驟可能相互交叉。你可以認為遞增操作如下:

讀取計數器的值。

加 1。

將新的值寫回計數器。

遞減操作的過程如下:

讀取計數器的值。

減 1。

將新的值寫回計數器。

現在我們考慮一下如下的執行步驟

第一個線程從主存中讀取計數器的值,初始值是 0,然后加 1。

第二個線程也從主存中讀取計數器的值,它讀取到的值也是 0,然后進行減 1 操作。

第一線程將新的計數器的值寫回內存,將值設置為 1。

第二個線程也將新的值寫回內存,將值設置為 -1。

怎么防止這類事件的發生?

使用同步:

public class SynchronizedCounter {

private int counter;

public synchronized void inc() {

counter++;

}

public synchronized void dec() {

counter--;

}

public synchronized int get() {

return counter;

}

}

或者使用 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,因為 synchronized 只允許一個線程訪問 inc/get/get 方法,對性能影響較大。

我注意到采用 Synchronized 的版本并沒有將計數器標識為 volatile,難道這意味著……?

對的。使用 synchronized 關鍵字也會在語句之間建立 happens-before 關系。進入一個同步方法或塊時,會將之前的語句和該方法或塊內部的語句建立 happens-before 關系。

查看完整的建立 happens-before 關系的情況列表,請查看這里。

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/package-summary.html#MemoryVisibility

關于一開始提到的 volatile, 這些是所有我想說的。所有的例子都上傳到了我的 github 倉庫。

https://github.com/sayembd/JavaSamples/commit/d1f72ee8bf76f69740b6d22e35d8e1431f279afb返回搜狐,查看更多

責任編輯:

總結

以上是生活随笔為你收集整理的java volatile 死锁_Java 多线程:volatile 变量、happens-before 关系及内存一致性的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。