java 多线程变量可见性_Java多线程:易变变量,事前关联和内存一致性
java 多線程變量可見(jiàn)性
什么是volatile變量?
volatile是Java中的關(guān)鍵字。 您不能將其用作變量或方法名稱(chēng)。 期。
我們什么時(shí)候應(yīng)該使用它?
哈哈,對(duì)不起,沒(méi)辦法。
當(dāng)我們?cè)诙嗑€程環(huán)境中與多個(gè)線程共享變量時(shí),通常使用volatile關(guān)鍵字,并且我們希望避免由于這些變量在CPU高速緩存中的緩存而導(dǎo)致任何內(nèi)存不一致錯(cuò)誤 。
考慮下面的生產(chǎn)者/消費(fèi)者示例,其中我們一次生產(chǎn)/消費(fèi)一件商品:
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;} }在上述類(lèi)中, Produce方法通過(guò)將其參數(shù)存儲(chǔ)到value中并將hasValue標(biāo)志更改為true來(lái)生成一個(gè)新值。 while循環(huán)檢查值標(biāo)志( hasValue )是否為true,這表示存在尚未使用的新值,如果為true,則請(qǐng)求當(dāng)前線程進(jìn)入睡眠狀態(tài)。 僅當(dāng)hasValue標(biāo)志已更改為false時(shí),此睡眠循環(huán)才會(huì)停止,這僅在consumer方法使用了新值時(shí)才有可能。 如果沒(méi)有新值可用,那么消耗方法將請(qǐng)求當(dāng)前線程Hibernate。 當(dāng)Produce方法產(chǎn)生一個(gè)新值時(shí),它將終止其睡眠循環(huán),使用它并清除value標(biāo)志。
現(xiàn)在想象一下,有兩個(gè)線程正在使用此類(lèi)的對(duì)象–一個(gè)正在嘗試產(chǎn)生值(寫(xiě)線程),另一個(gè)正在使用它們(讀線程)。 以下測(cè)試說(shuō)明了這種方法:
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();} }該示例在大多數(shù)情況下將產(chǎn)生預(yù)期的輸出,但也很有可能陷入僵局!
怎么樣?
讓我們談?wù)動(dòng)?jì)算機(jī)體系結(jié)構(gòu)。
我們知道計(jì)算機(jī)由CPU和內(nèi)存單元(以及許多其他部件)組成。 即使主存儲(chǔ)器是我們所有程序指令和變量/數(shù)據(jù)所在的位置,CPU仍可以在程序執(zhí)行期間將變量的副本存儲(chǔ)在其內(nèi)部存儲(chǔ)器(稱(chēng)為CPU緩存)中,以提高性能。 由于現(xiàn)代計(jì)算機(jī)現(xiàn)在具有不止一個(gè)CPU,因此也有不止一個(gè)CPU緩存。
在多線程環(huán)境中,可能有多個(gè)線程同時(shí)執(zhí)行,每個(gè)線程都在不同的CPU中運(yùn)行(盡管這完全取決于底層操作系統(tǒng)),并且每個(gè)線程都可以從main復(fù)制變量。內(nèi)存放入相應(yīng)的CPU緩存中。 當(dāng)線程訪問(wèn)這些變量時(shí),它們隨后將訪問(wèn)這些緩存的副本,而不是主內(nèi)存中的實(shí)際副本。
現(xiàn)在,假設(shè)測(cè)試中的兩個(gè)線程在兩個(gè)不同的CPU上運(yùn)行,并且hasValue標(biāo)志已緩存在其中一個(gè)(或兩個(gè))上。 現(xiàn)在考慮以下執(zhí)行順序:
僅當(dāng)hasValue標(biāo)志跨所有緩存同步時(shí),這種情況才會(huì)改變,這完全取決于基礎(chǔ)操作系統(tǒng)。
volatile如何適合此示例?
如果僅將hasValue標(biāo)志標(biāo)記為volatile ,則可以確保不會(huì)發(fā)生這種類(lèi)型的死鎖:
private volatile boolean hasValue = false;將變量標(biāo)記為volatile將迫使每個(gè)線程直接從主內(nèi)存中讀取該變量的值。 而且,每次對(duì)volatile變量的寫(xiě)操作都會(huì)立即刷新到主存儲(chǔ)器中。 如果線程決定緩存該變量,則它將在每次讀/寫(xiě)時(shí)與主內(nèi)存同步。
進(jìn)行此更改之后,請(qǐng)考慮導(dǎo)致死鎖的先前執(zhí)行步驟:
瞧! 我們都很高興^ _ ^!
這是否所有的易失性行為都迫使線程直接從內(nèi)存中讀取/寫(xiě)入變量?
實(shí)際上,它還具有其他含義。 訪問(wèn)易失性變量會(huì)在程序語(yǔ)句之間建立先發(fā)生后關(guān)系。
什么是
兩個(gè)程序語(yǔ)句之間的先發(fā)生后關(guān)系是一種保證,可確保一個(gè)語(yǔ)句寫(xiě)的任何內(nèi)存對(duì)另一條語(yǔ)句可見(jiàn)。
它與
當(dāng)我們寫(xiě)入一個(gè)易失性變量時(shí),它會(huì)在以后每次讀取該相同變量時(shí)創(chuàng)建一個(gè)事前發(fā)生的關(guān)系。 因此,在對(duì)該易失性變量進(jìn)行寫(xiě)操作之前執(zhí)行的所有內(nèi)存寫(xiě)操作,對(duì)于該易失性變量的讀取之后的所有語(yǔ)句,隨后都將可見(jiàn)。
Err..Ok ....我明白了,但也許是一個(gè)很好的例子。
好的,對(duì)模糊的定義表示抱歉。 考慮以下示例:
// 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假設(shè)上面的兩個(gè)代碼片段由兩個(gè)不同的線程(線程1和2)執(zhí)行。當(dāng)?shù)谝粋€(gè)線程更改hasValue時(shí) ,它不僅會(huì)將此更改刷新到主內(nèi)存,還將導(dǎo)致前三個(gè)寫(xiě)操作(以及其他任何寫(xiě)操作)先前的寫(xiě)入)也要刷新到主存儲(chǔ)器中! 結(jié)果,當(dāng)?shù)诙€(gè)線程訪問(wèn)這三個(gè)變量時(shí),它將看到線程1進(jìn)行的所有寫(xiě)操作,即使它們之前都已被緩存(這些緩存的副本也將被更新)!
這就是為什么我們?cè)诘谝粋€(gè)示例中也不必用volatile標(biāo)記值變量的原因。 由于我們?cè)谠L問(wèn)hasValue之前已寫(xiě)入該變量,并在讀取hasValue之后對(duì)其進(jìn)行了讀取,因此該變量會(huì)自動(dòng)與主內(nèi)存同步。
這還有另一個(gè)有趣的結(jié)果。 JVM以其程序優(yōu)化而聞名。 有時(shí),它在不更改程序輸出的情況下重新排列程序語(yǔ)句以提高性能。 例如,它可以更改以下語(yǔ)句序列:
first = 5; second = 6; third = 7;到這個(gè):
second = 6; third = 7; first = 5;但是,當(dāng)語(yǔ)句涉及訪問(wèn)volatile變量時(shí),它將永遠(yuǎn)不會(huì)移動(dòng)發(fā)生在volatile寫(xiě)入之后的語(yǔ)句。 這意味著它將永遠(yuǎn)不會(huì)改變:
first = 5; // write before volatile write second = 6; // write before volatile write third = 7; // write before volatile write hasValue = true;到這個(gè):
first = 5; second = 6; hasValue = true; third = 7; // Order changed to appear after volatile write! This will never happen!即使從程序正確性的角度來(lái)看,它們似乎都是等效的。 請(qǐng)注意,只要它們都出現(xiàn)在易失性寫(xiě)入之前,仍然允許JVM在它們之間對(duì)前三個(gè)寫(xiě)入進(jìn)行重新排序。
同樣,JVM也不會(huì)更改在讀取易失性變量后出現(xiàn)在訪問(wèn)之前的語(yǔ)句的順序。 這意味著:
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絕不會(huì)將其轉(zhuǎn)換為:
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可以肯定它們中最后三個(gè)讀取的順序,只要它們?cè)诳勺冏x取之后一直出現(xiàn)。
我認(rèn)為必須為易失性變量付出性能損失。
您說(shuō)對(duì)了,因?yàn)橐资宰兞繒?huì)強(qiáng)制訪問(wèn)主內(nèi)存,并且訪問(wèn)主內(nèi)存總是比訪問(wèn)CPU緩存慢。 它還會(huì)阻止JVM對(duì)某些程序進(jìn)行優(yōu)化,從而進(jìn)一步降低性能。
我們是否可以始終使用易變變量來(lái)維護(hù)線程之間的數(shù)據(jù)一致性?
不幸的是沒(méi)有。 當(dāng)多個(gè)線程讀寫(xiě)同一變量時(shí),將其標(biāo)記為volatile不足以保持一致性。 考慮以下UnsafeCounter類(lèi):
public class UnsafeCounter {private volatile int counter;public void inc() {counter++;}public void dec() {counter--;}public int get() {return counter;} }和以下測(cè)試:
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());} }該代碼是不言自明的。 我們?cè)谝粋€(gè)線程中遞增計(jì)數(shù)器,而在另一個(gè)線程中遞減計(jì)數(shù)器相同次數(shù)。 運(yùn)行此測(cè)試后,我們希望計(jì)數(shù)器保持0,但這不能保證。 在大多數(shù)情況下,它將為0,在某些情況下,它將為-1,-2、1、2,即[-5、5]范圍內(nèi)的任何整數(shù)值。
為什么會(huì)這樣? 發(fā)生這種情況是因?yàn)橛?jì)數(shù)器的遞增和遞減操作都不是原子的-它們不會(huì)一次全部發(fā)生。 它們都由多個(gè)步驟組成,并且步驟順序相互重疊。 因此,您可以考慮以下增量操作:
遞減操作如下:
現(xiàn)在,讓我們考慮以下執(zhí)行步驟:
我們?nèi)绾畏乐惯@種情況?
通過(guò)使用同步:
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();} }我個(gè)人的選擇是使用AtomicInteger作為同步對(duì)象,因?yàn)橹挥幸粋€(gè)線程可以訪問(wèn)任何inc / dec / get方法,從而大大降低了性能。
意思是不是……..?
對(duì)。 使用synced關(guān)鍵字還可以建立語(yǔ)句之間的事前發(fā)生關(guān)系。 輸入同步的方法/塊將在它之前出現(xiàn)的語(yǔ)句與該方法/塊內(nèi)部的語(yǔ)句之間建立先發(fā)生后關(guān)系。 有關(guān)建立事前關(guān)系的完整列表,請(qǐng)轉(zhuǎn)到此處 。
就暫時(shí)而言,這就是我要說(shuō)的。
- 所有示例都已上傳到我的github存儲(chǔ)庫(kù)中 。
翻譯自: https://www.javacodegeeks.com/2015/11/java-multi-threading-volatile-variables-happens-before-relationship-and-memory-consistency.html
java 多線程變量可見(jiàn)性
總結(jié)
以上是生活随笔為你收集整理的java 多线程变量可见性_Java多线程:易变变量,事前关联和内存一致性的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: a4大小 a4大小是怎样的
- 下一篇: java 微型数据库_Java 9代码工