java 多线程变量可见性_Java多线程:易变变量,事前关联和内存一致性
java 多線程變量可見性
什么是volatile變量?
volatile是Java中的關鍵字。 您不能將其用作變量或方法名稱。 期。
我們什么時候應該使用它?
哈哈,對不起,沒辦法。
當我們在多線程環(huán)境中與多個線程共享變量時,通常使用volatile關鍵字,并且我們希望避免由于這些變量在CPU高速緩存中的緩存而導致任何內存不一致錯誤 。
考慮下面的生產者/消費者示例,其中我們一次生產/消費一件商品:
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循環(huán)檢查值標志( hasValue )是否為true,這表示存在尚未使用的新值,如果為true,則請求當前線程進入睡眠狀態(tài)。 僅當hasValue標志已更改為false時,此睡眠循環(huán)才會停止,這僅在consumer方法使用了新值時才有可能。 如果沒有新值可用,那么消耗方法將請求當前線程Hibernate。 當Produce方法產生一個新值時,它將終止其睡眠循環(huán),使用它并清除value標志。
現在想象一下,有兩個線程正在使用此類的對象–一個正在嘗試產生值(寫線程),另一個正在使用它們(讀線程)。 以下測試說明了這種方法:
public class ProducerConsumerTest {@Testpublic 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仍可以在程序執(zhí)行期間將變量的副本存儲在其內部存儲器(稱為CPU緩存)中,以提高性能。 由于現代計算機現在具有不止一個CPU,因此也有不止一個CPU緩存。
在多線程環(huán)境中,可能有多個線程同時執(zhí)行,每個線程都在不同的CPU中運行(盡管這完全取決于底層操作系統(tǒng)),并且每個線程都可以從main復制變量。內存放入相應的CPU緩存中。 當線程訪問這些變量時,它們隨后將訪問這些緩存的副本,而不是主內存中的實際副本。
現在,假設測試中的兩個線程在兩個不同的CPU上運行,并且hasValue標志已緩存在其中一個(或兩個)上。 現在考慮以下執(zhí)行順序:
僅當hasValue標志跨所有緩存同步時,這種情況才會改變,這完全取決于基礎操作系統(tǒng)。
volatile如何適合此示例?
如果僅將hasValue標志標記為volatile ,則可以確保不會發(fā)生這種類型的死鎖:
private volatile boolean hasValue = false;將變量標記為volatile將迫使每個線程直接從主內存中讀取該變量的值。 而且,每次對volatile變量的寫操作都會立即刷新到主存儲器中。 如果線程決定緩存該變量,則它將在每次讀/寫時與主內存同步。
進行此更改之后,請考慮導致死鎖的先前執(zhí)行步驟:
瞧! 我們都很高興^ _ ^!
這是否所有的易失性行為都迫使線程直接從內存中讀取/寫入變量?
實際上,它還具有其他含義。 訪問易失性變量會在程序語句之間建立先發(fā)生后關系。
什么是
兩個程序語句之間的先發(fā)生后關系是一種保證,可確保一個語句寫的任何內存對另一條語句可見。
它與
當我們寫入一個易失性變量時,它會在以后每次讀取該相同變量時創(chuàng)建一個事前發(fā)生的關系。 因此,在對該易失性變量進行寫操作之前執(zhí)行的所有內存寫操作,對于該易失性變量的讀取之后的所有語句,隨后都將可見。
Err..Ok ....我明白了,但也許是一個很好的例子。
好的,對模糊的定義表示抱歉。 考慮以下示例:
// 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)執(zhí)行。當第一個線程更改hasValue時 ,它不僅會將此更改刷新到主內存,還將導致前三個寫操作(以及其他任何寫操作)先前的寫入)也要刷新到主存儲器中! 結果,當第二個線程訪問這三個變量時,它將看到線程1進行的所有寫操作,即使它們之前都已被緩存(這些緩存的副本也將被更新)!
這就是為什么我們在第一個示例中也不必用volatile標記值變量的原因。 由于我們在訪問hasValue之前已寫入該變量,并在讀取hasValue之后對其進行了讀取,因此該變量會自動與主內存同步。
這還有另一個有趣的結果。 JVM以其程序優(yōu)化而聞名。 有時,它在不更改程序輸出的情況下重新排列程序語句以提高性能。 例如,它可以更改以下語句序列:
first = 5; second = 6; third = 7;到這個:
second = 6; third = 7; first = 5;但是,當語句涉及訪問volatile變量時,它將永遠不會移動發(fā)生在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在它們之間對前三個寫入進行重新排序。
同樣,JVM也不會更改在讀取易失性變量后出現在訪問之前的語句的順序。 這意味著:
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 readJVM絕不會將其轉換為:
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可以肯定它們中最后三個讀取的順序,只要它們在可變讀取之后一直出現。
我認為必須為易失性變量付出性能損失。
您說對了,因為易失性變量會強制訪問主內存,并且訪問主內存總是比訪問CPU緩存慢。 它還會阻止JVM對某些程序進行優(yōu)化,從而進一步降低性能。
我們是否可以始終使用易變變量來維護線程之間的數據一致性?
不幸的是沒有。 當多個線程讀寫同一變量時,將其標記為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 {@Testpublic 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]范圍內的任何整數值。
為什么會這樣? 發(fā)生這種情況是因為計數器的遞增和遞減操作都不是原子的-它們不會一次全部發(fā)生。 它們都由多個步驟組成,并且步驟順序相互重疊。 因此,您可以考慮以下增量操作:
遞減操作如下:
現在,讓我們考慮以下執(zhí)行步驟:
我們如何防止這種情況?
通過使用同步:
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作為同步對象,因為只有一個線程可以訪問任何inc / dec / get方法,從而大大降低了性能。
意思是不是……..?
對。 使用synced關鍵字還可以建立語句之間的事前發(fā)生關系。 輸入同步的方法/塊將在它之前出現的語句與該方法/塊內部的語句之間建立先發(fā)生后關系。 有關建立事前關系的完整列表,請轉到此處 。
就暫時而言,這就是我要說的。
- 所有示例都已上傳到我的github存儲庫中 。
翻譯自: https://www.javacodegeeks.com/2015/11/java-multi-threading-volatile-variables-happens-before-relationship-and-memory-consistency.html
java 多線程變量可見性
總結
以上是生活随笔為你收集整理的java 多线程变量可见性_Java多线程:易变变量,事前关联和内存一致性的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: a4大小 a4大小是怎样的
- 下一篇: 微分的定义是什么 什么是微分