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

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程语言 > java >内容正文

java

java 多线程变量可见性_Java多线程:易变变量,事前关联和内存一致性

發(fā)布時(shí)間:2023/12/3 java 37 豆豆
生活随笔 收集整理的這篇文章主要介紹了 java 多线程变量可见性_Java多线程:易变变量,事前关联和内存一致性 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

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í)行順序:

  • writerThread產(chǎn)生一個(gè)值,并將hasValue更改為true。 但是,此更新僅反映在緩存中,而不反映在主存儲(chǔ)器中。
  • readerThread嘗試使用一個(gè)值,但是hasValue標(biāo)志的緩存副本設(shè)置為false。 因此,即使writerThread產(chǎn)生了一個(gè)值,它也無(wú)法使用它,因?yàn)榫€程無(wú)法脫離睡眠循環(huán)( hasValue為false)。
  • 由于readerThread沒(méi)有使用新生成的值, writerThread不能繼續(xù)進(jìn)行,因?yàn)樵摌?biāo)志沒(méi)有被清除,因此它將停留在其Hibernate循環(huán)中。
  • 而且我們手中有一個(gè)僵局!
  • 僅當(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í)行步驟:

  • 作家線程 ? 產(chǎn)生一個(gè)值,并將hasValue更改為true。 這次更新將直接反映到主內(nèi)存中(即使已緩存)。
  • 讀取器線程正在嘗試使用一個(gè)值,并檢查hasValue的值。 這次,每次讀取都將強(qiáng)制直接從主內(nèi)存中獲取值,因此它將獲取寫(xiě)入線程所做的更改。
  • 閱讀器線程使用生成的值,并清除標(biāo)志的值。 這個(gè)新值將進(jìn)入主內(nèi)存(如果已緩存,則緩存的副本也將被更新)。
  • 編寫(xiě)器線程將接受此更改,因?yàn)槊總€(gè)讀取現(xiàn)在都在訪問(wèn)主內(nèi)存。 它將繼續(xù)產(chǎn)生新的價(jià)值。
  • 瞧! 我們都很高興^ _ ^!

    這是否所有的易失性行為都迫使線程直接從內(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 read

    JVM絕不會(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è)步驟組成,并且步驟順序相互重疊。 因此,您可以考慮以下增量操作:

  • 讀取計(jì)數(shù)器的值。
  • 添加一個(gè)。
  • 寫(xiě)回計(jì)數(shù)器的新值。
  • 遞減操作如下:

  • 讀取計(jì)數(shù)器的值。
  • 從中減去一個(gè)。
  • 寫(xiě)回計(jì)數(shù)器的新值。
  • 現(xiàn)在,讓我們考慮以下執(zhí)行步驟:

  • 第一個(gè)線程已從內(nèi)存中讀取計(jì)數(shù)器的值。 最初它設(shè)置為零。 然后向其中添加一個(gè)。
  • 第二個(gè)線程還從內(nèi)存中讀取了該計(jì)數(shù)器的值,并看到將其設(shè)置為零。 然后從中減去一個(gè)。
  • 現(xiàn)在,第一個(gè)線程將counter的新值寫(xiě)回內(nèi)存,將其更改為1。
  • 現(xiàn)在,第二個(gè)線程將計(jì)數(shù)器的新值寫(xiě)回內(nèi)存,即-1。
  • 第一線程的更新丟失。
  • 我們?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)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。