這篇文章主要介紹了如何解決volatile和happens-before的關(guān)系與內(nèi)存一致性錯(cuò)誤,具有一定借鑒價(jià)值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。
創(chuàng)新互聯(lián)-專(zhuān)業(yè)網(wǎng)站定制、快速模板網(wǎng)站建設(shè)、高性?xún)r(jià)比鄂溫克網(wǎng)站開(kāi)發(fā)、企業(yè)建站全套包干低至880元,成熟完善的模板庫(kù),直接使用。一站式鄂溫克網(wǎng)站制作公司更省心,省錢(qián),快速模板網(wǎng)站建設(shè)找我們,業(yè)務(wù)覆蓋鄂溫克地區(qū)。費(fèi)用合理售后完善,10余年實(shí)體公司更值得信賴(lài)。
volatile變量
volatile是Java的關(guān)鍵詞,我們可以用它來(lái)修飾變量或者方法。
為什么要使用volatile
volatile的典型用法是,當(dāng)多個(gè)線(xiàn)程共享變量,且我們要避免由于內(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ù)還沒(méi)有被消費(fèi),如果為true,則休眠當(dāng)前線(xiàn)程。當(dāng)hasValue置為false的時(shí)候,休眠循環(huán)才會(huì)停止,也就是將數(shù)據(jù)被consume方法消費(fèi)后。如果沒(mé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è)線(xiàn)程使用該類(lèi)的同一個(gè)對(duì)象——一個(gè)用來(lái)產(chǎn)生數(shù)據(jù)(write線(xiàn)程),另一個(gè)用來(lái)消耗數(shù)據(jù)(read線(xiàn)程)。實(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緩沖。
在多線(xiàn)程環(huán)境中,多個(gè)線(xiàn)程有可能在同一個(gè)時(shí)間運(yùn)行,每個(gè)在不同的CPU(由底層OS決定),并且他們可能從主內(nèi)存中復(fù)制變量到對(duì)應(yīng)的CPU緩沖中。當(dāng)線(xiàn)程訪問(wèn)這些變量時(shí),其訪問(wèn)的是這些緩沖的變量,并不是位于主內(nèi)存的實(shí)際變量。
現(xiàn)在我們假設(shè)上個(gè)例子中的兩個(gè)線(xiàn)程運(yùn)行在兩個(gè)不同的CPU上,并且hasValue變量被緩沖在其中一個(gè)CPU上(或者兩個(gè))。考慮以下的執(zhí)行序列:
1.writer線(xiàn)程產(chǎn)生一個(gè)數(shù)據(jù),并將hasValue設(shè)置為true。然而,這個(gè)改變只是體現(xiàn)在CPU緩沖上,而不是主內(nèi)存。
2.reader線(xiàn)程準(zhǔn)備消耗一個(gè)數(shù)據(jù),但是其CPU緩沖的hasValue為false。所以即使writer線(xiàn)程產(chǎn)生了一個(gè)數(shù)據(jù),reader線(xiàn)程也不能消耗該數(shù)據(jù)。
3.由于reader線(xiàn)程無(wú)法消費(fèi)新產(chǎn)生的數(shù)據(jù),writer線(xiàn)程也不能繼續(xù)產(chǎn)生新的數(shù)據(jù)(由于hasValue為true),因此writer會(huì)休眠。
4.然后就出現(xiàn)了死鎖!
當(dāng)hasValue值在所有的緩沖中都同步(基于底層OS),該情形就會(huì)改變。
解決方案?volatile如何適用該例子?
如果我們將hasValue設(shè)置為volatile,那么我們可以保證這種類(lèi)型的死鎖不會(huì)出現(xiàn)。
private volatile boolean hasValue = false;
將一個(gè)變量設(shè)置為volatile后,線(xiàn)程就會(huì)直接從主內(nèi)存中讀取該變量的值,并且該變量的寫(xiě)入會(huì)立即刷新到主內(nèi)存中。如果一個(gè)線(xiàn)程緩沖了該變量,那么每次讀和寫(xiě)操作都會(huì)和主內(nèi)存同步。
這個(gè)修改后,考慮上面那個(gè)可能會(huì)導(dǎo)致死鎖的步驟:
1.writer產(chǎn)生了一個(gè)新的數(shù)據(jù),并將hasValue設(shè)置為true。該更新會(huì)直接反映在主內(nèi)存中(即使該線(xiàn)程使用了緩存)。
2.reader線(xiàn)程嘗試消費(fèi)一個(gè)變量,并檢查hasValue的值。該變量的每次讀都會(huì)直接從主內(nèi)存獲得,所以它能獲得到writer線(xiàn)程導(dǎo)致的改變。
3.reader線(xiàn)程消費(fèi)該變量并清楚hasValue標(biāo)志位。該變量會(huì)刷新到主內(nèi)存中(如果被緩存,則緩存的變量也會(huì)刷新)。
4.由于reader線(xiàn)程每次都操作的主內(nèi)存,所以writer線(xiàn)程能看到reader導(dǎo)致的改變。其會(huì)繼續(xù)產(chǎn)生新的數(shù)據(jù)。
volatile與happens-before關(guān)系
訪問(wèn)volatile變量在語(yǔ)句間建立了happens-before關(guān)系。當(dāng)寫(xiě)入一個(gè)volatile變量時(shí),它與之后的該變量的讀操作建立了happens-before關(guān)系。那么什么是happens-before關(guān)系呢?可以參考筆者之前的博客[Java并發(fā)編程番外篇(二)happens-before關(guān)系],簡(jiǎn)單來(lái)說(shuō),就是保證一個(gè)語(yǔ)句的影響會(huì)被另一個(gè)語(yǔ)句看到(https://www.jb51.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è)線(xiàn)程——線(xiàn)程1和線(xiàn)程2. 當(dāng)線(xiàn)程1修改hasValue值后,不僅僅hasValue的值會(huì)直接寫(xiě)入到主內(nèi)存,前面的三個(gè)寫(xiě)操作也會(huì)寫(xiě)入主內(nèi)存(和之前的其他寫(xiě)操作)。因此,當(dāng)線(xiàn)程2訪問(wèn)這三個(gè)變量時(shí),它會(huì)看到線(xiàn)程1對(duì)這些變量進(jìn)行的修改,即使他們會(huì)緩存(這些緩存也會(huì)被更新)。
這也正是在第一個(gè)例子中,我們沒(méi)有將value變量設(shè)置為volatile的原因。這是由于訪問(wèn)hasValue之前其他變量的寫(xiě)操作,和讀hashValue之后其他變量的讀操作,會(huì)自動(dòng)和主內(nèi)存同步。
這是另外一個(gè)有趣的序列。JVM以它的程序優(yōu)化著名。有時(shí)候,在不影響輸出的情況下,JVM會(huì)對(duì)指令進(jìn)行重排序來(lái)獲得更好的性能。作為例子,它可能將該序列的代碼,
first = 5; second = 6; third = 7;
重排序?yàn)椋?/p>
first = 5; second = 6; third = 7;
然而,當(dāng)一個(gè)語(yǔ)句涉及到訪問(wèn)volatile變量,那么JVM就不會(huì)將一個(gè)volatile寫(xiě)操作之前的語(yǔ)句放到volatile寫(xiě)操作之后。也就是說(shuō),它不會(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!
即使從代碼正確性的角度來(lái)看,這兩者是相同的。注意到JVM仍然允許重排序前三條語(yǔ)句,只要他們位于volatile寫(xiě)之前。
類(lèi)似,JVM不會(huì)將位于volatile讀之后的代碼重排序到volatile讀之前。也就是說(shuō)該代碼,
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可以將后三條語(yǔ)句重排序,只要他們?cè)趘olatile讀之后。
volatile帶來(lái)的性能開(kāi)銷(xiāo)
volatile強(qiáng)制進(jìn)行主內(nèi)存訪問(wèn),而主內(nèi)存訪問(wèn)通常比CPU緩存訪問(wèn)慢。同時(shí)也阻止了JVM進(jìn)行的一些程序優(yōu)化,更進(jìn)一步降低了性能。
能否使用volatile來(lái)保證多線(xiàn)程的數(shù)據(jù)一致性?
答案是不能。當(dāng)多個(gè)線(xiàn)程訪問(wèn)同一個(gè)變量時(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 UnsafeCounter { private volatile int counter; public void inc() { counter++; } public void dec() { counter--; } public int get() { return counter; } }
代碼很容易讀懂。我們?cè)谝粋€(gè)線(xiàn)程中增加計(jì)數(shù)器的值,然后在另一個(gè)線(xiàn)程中減少計(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ù)值寫(xiě)入到counter中
同樣的,自減操作:
1.讀取counter數(shù)值
2.減少1
3.將數(shù)值寫(xiě)入到counter中
現(xiàn)在,我們考慮以下的執(zhí)行序列:
1.第一個(gè)線(xiàn)程從內(nèi)存中讀取counter的值。其被初始化為0. 然后該線(xiàn)程將其自增.
2.第二個(gè)線(xiàn)程同時(shí)也從內(nèi)存中讀取counter的值,并且該值也為0. 然后該線(xiàn)程對(duì)其執(zhí)行自減操作。
3.第一個(gè)進(jìn)程將數(shù)值寫(xiě)入到內(nèi)存中,即,counter的值為1.
4.第二個(gè)線(xiàn)程將數(shù)值寫(xiě)入到內(nèi)存中,即,counter的值為-1.
5.第一個(gè)線(xiàn)程的更新被丟失。
怎么阻止該現(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è)線(xiàn)程訪問(wèn)inc/dec/get方法,這帶來(lái)了額外的性能開(kāi)銷(xiāo)。
使用同步方法時(shí),我們并沒(méi)有將counter設(shè)置為volatile變量。這是因?yàn)椋褂胹ynchronized關(guān)鍵詞就建立了happens-before關(guān)系。進(jìn)入一個(gè)同步方法(代碼塊),在該語(yǔ)句之前的代碼和方法(代碼塊)中的代碼建立了happens-before關(guān)系。
感謝你能夠認(rèn)真閱讀完這篇文章,希望小編分享的“如何解決volatile和happens-before的關(guān)系與內(nèi)存一致性錯(cuò)誤”這篇文章對(duì)大家有幫助,同時(shí)也希望大家多多支持創(chuàng)新互聯(lián),關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,更多相關(guān)知識(shí)等著你來(lái)學(xué)習(xí)!
網(wǎng)站名稱(chēng):如何解決volatile和happens-before的關(guān)系與內(nèi)存一致性錯(cuò)誤
分享URL:http://m.newbst.com/article10/ppiedo.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供服務(wù)器托管、虛擬主機(jī)、網(wǎng)站制作、企業(yè)網(wǎng)站制作、App開(kāi)發(fā)、
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶(hù)投稿、用戶(hù)轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話(huà):028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)