最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

volatile與happens-before的關(guān)系與內(nèi)存一致性錯(cuò)誤

 更新時(shí)間:2019年05月21日 10:51:40   作者:fabowang  
本文講了Java并發(fā)編程中volatile變量、happens-before與內(nèi)存一致性錯(cuò)誤,下面來和小編一起學(xué)習(xí)下如何解決

volatile變量

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

為什么要使用volatile
volatile的典型用法是,當(dāng)多個(gè)線程共享變量,且我們要避免由于內(nèi)存緩沖變量導(dǎo)致的內(nèi)存一致性(Memory Consistency Errors)錯(cuò)誤時(shí)。

考慮以下的生產(chǎn)者消費(fèi)者例子,在一個(gè)時(shí)刻我們生產(chǎn)或消費(fèi)一個(gè)單位。

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;
 }
}

在這個(gè)例子中,produce方法產(chǎn)生一個(gè)新的值,并保存在value變量中,并且將hasValue標(biāo)志位置為true。while循環(huán)檢查hasValue是否為true,為true則標(biāo)志產(chǎn)生的數(shù)據(jù)還沒有被消費(fèi),如果為true,則休眠當(dāng)前線程。當(dāng)hasValue置為false的時(shí)候,休眠循環(huán)才會(huì)停止,也就是將數(shù)據(jù)被consume方法消費(fèi)后。如果沒有可用的數(shù)據(jù),cosume方法會(huì)休眠。當(dāng)produce方法產(chǎn)生一個(gè)新的數(shù)據(jù)后,consume會(huì)結(jié)束休眠,消費(fèi)該數(shù)據(jù),并清除hasValue標(biāo)志位。

現(xiàn)在設(shè)想兩個(gè)線程使用該類的同一個(gè)對(duì)象——一個(gè)用來產(chǎn)生數(shù)據(jù)(write線程),另一個(gè)用來消耗數(shù)據(jù)(read線程)。實(shí)例代碼如下,

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();
 }
}

在大多數(shù)情況下,該例子會(huì)輸出預(yù)期的結(jié)果,但是也有很大的可能進(jìn)入死鎖狀態(tài)!

為什么會(huì)發(fā)生該現(xiàn)象?

首先我們介紹一點(diǎn)計(jì)算機(jī)架構(gòu)的知識(shí)。

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

在多線程環(huán)境中,多個(gè)線程有可能在同一個(gè)時(shí)間運(yùn)行,每個(gè)在不同的CPU(由底層OS決定),并且他們可能從主內(nèi)存中復(fù)制變量到對(duì)應(yīng)的CPU緩沖中。當(dāng)線程訪問這些變量時(shí),其訪問的是這些緩沖的變量,并不是位于主內(nèi)存的實(shí)際變量。

現(xiàn)在我們假設(shè)上個(gè)例子中的兩個(gè)線程運(yùn)行在兩個(gè)不同的CPU上,并且hasValue變量被緩沖在其中一個(gè)CPU上(或者兩個(gè))??紤]以下的執(zhí)行序列:

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

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

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

如果我們將hasValue設(shè)置為volatile,那么我們可以保證這種類型的死鎖不會(huì)出現(xiàn)。

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

這個(gè)修改后,考慮上面那個(gè)可能會(huì)導(dǎo)致死鎖的步驟:

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

volatile與happens-before關(guān)系

訪問volatile變量在語句間建立了happens-before關(guān)系。當(dāng)寫入一個(gè)volatile變量時(shí),它與之后的該變量的讀操作建立了happens-before關(guān)系。那么什么是happens-before關(guān)系呢?可以參考筆者之前的博客[Java并發(fā)編程番外篇(二)happens-before關(guān)系],簡(jiǎn)單來說,就是保證一個(gè)語句的影響會(huì)被另一個(gè)語句看到(http://m.fzitv.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

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

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

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

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

重排序?yàn)椋?/p>

first = 5;
second = 6;
third = 7;
然而,當(dāng)一個(gè)語句涉及到訪問volatile變量,那么JVM就不會(huì)將一個(gè)volatile寫操作之前的語句放到volatile寫操作之后。也就是說,它不會(huì)將以下的代碼序列,
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不會(huì)將位于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

并不會(huì)修改為,


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可以將后三條語句重排序,只要他們?cè)趘olatile讀之后。

volatile帶來的性能開銷

volatile強(qiáng)制進(jìn)行主內(nèi)存訪問,而主內(nèi)存訪問通常比CPU緩存訪問慢。同時(shí)也阻止了JVM進(jìn)行的一些程序優(yōu)化,更進(jìn)一步降低了性能。

能否使用volatile來保證多線程的數(shù)據(jù)一致性?

答案是不能。當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí),將該變量標(biāo)志為volatile并不足以保證一致性,考慮下面的UnsafeCounter類,

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

測(cè)試代碼,

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

代碼很容易讀懂。我們?cè)谝粋€(gè)線程中增加計(jì)數(shù)器的值,然后在另一個(gè)線程中減少計(jì)數(shù)器的值。運(yùn)行這個(gè)測(cè)試,我們預(yù)期的計(jì)數(shù)器的結(jié)果是0,但是這并不能保證。大多數(shù)情況下都是0,然而,一些情況下,可能是-2,-1,1,2,甚至[-5,5]的任何數(shù)字。

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

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

同樣的,自減操作:

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

現(xiàn)在,我們考慮以下的執(zhí)行序列:

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

怎么阻止該現(xiàn)象呢?

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,因?yàn)橥椒椒ㄖ辉试S一個(gè)線程訪問inc/dec/get方法,這帶來了額外的性能開銷。

使用同步方法時(shí),我們并沒有將counter設(shè)置為volatile變量。這是因?yàn)?,使用synchronized關(guān)鍵詞就建立了happens-before關(guān)系。進(jìn)入一個(gè)同步方法(代碼塊),在該語句之前的代碼和方法(代碼塊)中的代碼建立了happens-before關(guān)系。淺談Java內(nèi)存模型之happens-before可以查看詳細(xì)介紹。

以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。

相關(guān)文章

  • java使用dom4j操作xml示例代碼

    java使用dom4j操作xml示例代碼

    dom4j是一個(gè)Java的XML API,類似于jdom,用來讀寫XML文件,下面我來個(gè)小例子學(xué)習(xí)他的使用方法
    2013-11-11
  • 深入理解Java編程線程池的實(shí)現(xiàn)原理

    深入理解Java編程線程池的實(shí)現(xiàn)原理

    這篇文章主要介紹了深入理解Java編程線程池的實(shí)現(xiàn)原理,涉及ThreadPoolExecutor類,線程池實(shí)現(xiàn)原理及示例等相關(guān)內(nèi)容,具有一定參考價(jià)值,需要的朋友可以了解下。
    2017-11-11
  • Java基礎(chǔ)教程之對(duì)象引用

    Java基礎(chǔ)教程之對(duì)象引用

    這篇文章主要介紹了Java基礎(chǔ)教程之對(duì)象引用,“對(duì)象引用”(object reference)是一個(gè)重要重要概念,涉及內(nèi)存,需要的朋友可以參考下
    2014-09-09
  • SpringBoot中Dozer的使用小結(jié)

    SpringBoot中Dozer的使用小結(jié)

    dozer是用來兩個(gè)對(duì)象之間屬性轉(zhuǎn)換的工具,有了這個(gè)工具之后,我們將一個(gè)對(duì)象的所有屬性值轉(zhuǎn)給另一個(gè)對(duì)象時(shí),就不需要再去寫重復(fù)的set和get方法了,下面介紹下SpringBoot中Dozer的使用,感興趣的朋友一起看看吧
    2022-03-03
  • 基于Java SSM實(shí)現(xiàn)Excel數(shù)據(jù)批量導(dǎo)入

    基于Java SSM實(shí)現(xiàn)Excel數(shù)據(jù)批量導(dǎo)入

    這篇文章主要為大家詳細(xì)介紹了基于Java SSM如何實(shí)現(xiàn)excel數(shù)據(jù)批量導(dǎo)入,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-11-11
  • 如何基于springboot-admin實(shí)現(xiàn)后臺(tái)監(jiān)控

    如何基于springboot-admin實(shí)現(xiàn)后臺(tái)監(jiān)控

    這篇文章主要介紹了如何基于springboot-admin實(shí)現(xiàn)后臺(tái)監(jiān)控,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-04-04
  • Spring框架核心概念小結(jié)

    Spring框架核心概念小結(jié)

    Spring是企業(yè)級(jí)Java的開源開發(fā)框架。Spring框架的核心功能可用于開發(fā)任何java應(yīng)用程序,本文重點(diǎn)給大家介紹Spring框架核心概念總覽,感興趣的朋友跟隨小編一起看看吧
    2022-02-02
  • Mybatis的xml文件時(shí)間范圍條件查詢方式

    Mybatis的xml文件時(shí)間范圍條件查詢方式

    這篇文章主要介紹了Mybatis的xml文件時(shí)間范圍條件查詢方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-03-03
  • Springboot整合ActiveMQ實(shí)現(xiàn)消息隊(duì)列的過程淺析

    Springboot整合ActiveMQ實(shí)現(xiàn)消息隊(duì)列的過程淺析

    昨天仔細(xì)研究了activeMQ消息隊(duì)列,也遇到了些坑,下面這篇文章主要給大家介紹了關(guān)于SpringBoot整合ActiveMQ的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2023-02-02
  • 使用OpenFeign實(shí)現(xiàn)服務(wù)調(diào)用的方法詳解

    使用OpenFeign實(shí)現(xiàn)服務(wù)調(diào)用的方法詳解

    OpenFeign是運(yùn)行在客戶端的聲明式服務(wù)調(diào)用的框架,通過聲明接口的方式來達(dá)到對(duì)服務(wù)的調(diào)用,本文就來和大家講講如何使用OpenFeign實(shí)現(xiàn)服務(wù)調(diào)用吧
    2023-06-06

最新評(píng)論

安福县| 固始县| 凤冈县| 永定县| 德安县| 辽阳县| 通城县| 新兴县| 华亭县| 绍兴县| 榆树市| 棋牌| 泸溪县| 丹寨县| 娄烦县| 嘉善县| 三河市| 榆树市| 子长县| 虞城县| 确山县| 天津市| 通化市| 隆尧县| 荔浦县| 海口市| 陕西省| 屯昌县| 法库县| 广宗县| 安国市| 崇左市| 全椒县| 得荣县| 司法| 安义县| 抚顺县| 罗定市| 屯门区| 钟山县| 泸水县|