摘要:有可能一個(gè)線程中的動(dòng)作相對(duì)于另一個(gè)線程出現(xiàn)亂序。當(dāng)實(shí)際輸出取決于線程交錯(cuò)的結(jié)果時(shí),這種情況被稱為競(jìng)爭(zhēng)條件。這里的問題在于代碼塊不是原子性的,而且實(shí)例的變化對(duì)別的線程不可見。這種不能同時(shí)在多個(gè)線程上執(zhí)行的部分被稱為關(guān)鍵部分。
為什么要額外寫一篇文章來研究volatile呢?是因?yàn)檫@可能是并發(fā)中最令人困惑以及最被誤解的結(jié)構(gòu)。我看過不少解釋volatile的博客,但是大多數(shù)要么不完整,要么難以理解。我會(huì)從并發(fā)中最重要的一些因素開始說起:
原子性
原子性是不可分割的操作。它們要么全部實(shí)現(xiàn),要么全部不實(shí)現(xiàn)。Java中原子操作的最佳例子是將一個(gè)值賦給變量。
可見性
可見性是指:無(wú)論是哪個(gè)線程對(duì)一個(gè)共享的變量作出的修改或是帶來的影響,讀其他的線程都是可見的。
有序性
有序性是指源碼中指令是否會(huì)被編譯器出于優(yōu)化而改變執(zhí)行順序。有可能一個(gè)線程中的動(dòng)作相對(duì)于另一個(gè)線程出現(xiàn)亂序。
現(xiàn)在舉一個(gè)例子來理解這些因素:
public class MyApp { private int count = 0; public void upateVisitors() { ++count; //increment the visitors count } }
Hint: read-modify-write
這一段代碼中有一個(gè)試圖更新應(yīng)用(網(wǎng)頁(yè))的訪客數(shù)量的方法。這段代碼的問題在于++count指令不是原子性的,它包含三條獨(dú)立的指令:
temp = count; (read) temp = temp + 1; (modify) count = temp; (write)
因此,當(dāng)一個(gè)線程正在執(zhí)行此操作時(shí),此指令可以被另一個(gè)線程預(yù)占。從而不是原子性操作。假設(shè)count的值為10,并且有如下的執(zhí)行順序:
我們會(huì)發(fā)現(xiàn):在某個(gè)很不巧合的時(shí)刻,兩個(gè)線程同時(shí)讀取到了值(10),然后彼此將其值加一。所以在這個(gè)過程有一個(gè)遞增的操作丟失了。當(dāng)實(shí)際輸出取決于線程交錯(cuò)的結(jié)果時(shí),這種情況被稱為競(jìng)爭(zhēng)條件(race condition)。這里丟失了一次遞增。那么并發(fā)的哪些方面在這里缺失了?原子性。再考慮一個(gè)創(chuàng)建單例的例子(當(dāng)然也是不好的例子):
public Singleton getInstance() { if(_instance == null) { _instance = new Singleton(); } }
Hint: check-then-act
再一次的,可能有兩個(gè)線程都判斷這實(shí)例為null,并且都進(jìn)入了if代碼塊。這會(huì)導(dǎo)致兩個(gè)實(shí)例的創(chuàng)建。這里的問題在于代碼塊不是原子性的,而且實(shí)例的變化對(duì)別的線程不可見。這種不能同時(shí)在多個(gè)線程上執(zhí)行的部分被稱為關(guān)鍵部分(critical section)。對(duì)于關(guān)鍵部分,我們需要使用synchronized塊和synchronized方法。
還是原子性
為了確保原子性,我們通常使用鎖來確保互斥。參考下面的例子,一個(gè)銀行賬戶使用synchronized方法上鎖。
class BankAccount { private int accountBalance; synchronized int getAccountBalance() { return accountBalance; } synchronized void setAccountBalance(int b) throws IllegalStateException { accountBalance = b; if (accountBalance < 0) { throw new IllegalStateException("Sorry but account has negative Balance"); } } void depositMoney(int amount) { int balance = getAccountBalance(); setAccountBalance(balance + amount); } void withdrawMoney(int amount) { int balance = getAccountBalance(); setAccountBalance(balance - amount); } }
對(duì)共享變量balance的訪問通過鎖來保護(hù),從而數(shù)據(jù)競(jìng)爭(zhēng)不會(huì)有問題。這個(gè)類有問題嗎?是有的。假設(shè)一個(gè)線程調(diào)用depositMoney(50)而另一個(gè)線程調(diào)用withdrawMoney(50),并且balance的初始值為100。理想情況下操作完成后balance應(yīng)該為0。但是我們無(wú)法保證得到這個(gè)結(jié)果:
depositMoney操作讀取的balance值為100
withdrawMoney操作讀取的balance值也是100,它在此基礎(chǔ)上減去50元并將其設(shè)為50元。
最終depositMoney在之前看到的balance值的基礎(chǔ)上加上50,并將其設(shè)為150。
再次因?yàn)闆]有保證原子性而丟失了一個(gè)更新。如果兩種方法都被聲明為同步,則將在整個(gè)方法期間確保鎖定,并且改變將以原子方式進(jìn)行。
再談可見性
如果一個(gè)線程的操作對(duì)另一個(gè)線程可見,那么其他線程也會(huì)觀察到它的所有操作的結(jié)果。考慮下面的例子:
public class LooperThread extends Thread { private boolean isDone = false; public void run() { while( !isDone ) { doSomeWork(); } } public void stopWork() { isDone = true; } }
這里缺失了什么?假設(shè)LooperThread的一個(gè)實(shí)例正在運(yùn)行,主線程調(diào)用了stopWord來中止它。這兩個(gè)線程之間沒有實(shí)現(xiàn)同步。編譯器會(huì)以為在第一個(gè)線程中沒有對(duì)isDone執(zhí)行寫入操作,并且決定只讀入isDone一次。于是,線程炸了!部分JVM可能會(huì)這樣做,從而使其變成無(wú)限循環(huán)。因此答案顯然是缺乏可見性。
再談?dòng)行蛐?/strong>
有序性是關(guān)于事情發(fā)生的順序。考慮下面的例子:
在上述情況下,線程2能打印出value = 0嗎?其實(shí)是有可能的。在編譯器重新排序中result=true可能會(huì)在value=1之前出現(xiàn)。value = 1也可能不對(duì)線程2可見,然后線程2將加載value = 0。我們可以使用volatile解決這個(gè)問題嗎?
CPU架構(gòu)(多層RAMs)
CPU現(xiàn)在通常多核,并且線程將在不同核心上運(yùn)行。另外還有不同級(jí)別的高速緩存,如下圖所示:
當(dāng)一個(gè)volatile變量被任何線程寫入一個(gè)特定的核心,所有其他核心的值都需要更新,因?yàn)槊總€(gè)核心都有其自己的緩存,該緩存內(nèi)有變量的舊值。消息傳遞給所有內(nèi)核以更新值。
volatile
根據(jù)Java文檔,如果一個(gè)變量被聲明為volatile,那么Java內(nèi)存模型(在JDK 5之后)確保所有線程都看到變量的一致值。volatile就像是synchronized的一個(gè)親戚,讀取volatile數(shù)據(jù)就像是進(jìn)入一個(gè)synchronized塊,而寫入volatile數(shù)據(jù)就像是從synchronized塊中離開。當(dāng)寫入一個(gè)volatile值時(shí),這個(gè)值直接寫入主存而不是本地處理器的緩存,并且通過發(fā)送消息提醒其它內(nèi)核的緩存該值的更新。Volatile不是原子性操作
volatile保證順序性和可見性但是不保證互斥或是原子性。鎖能保證原子性,可視性和順序性。所以volatile不能代替synchronized。
volatile讀與寫
volatile提供了順序性保障,這意味著編譯器生成的指令不能以實(shí)際源代碼指令定義的順序以外的其他順序執(zhí)行操作結(jié)果。盡管生成的指令的順序可能與源代碼的原始順序不同,但所產(chǎn)生的效果必須相同。我們還需要從Java Doc中觀察以下關(guān)于讀寫的內(nèi)容:
當(dāng)一個(gè)線程讀取一個(gè)volatile變量時(shí),它不僅會(huì)看到volatile的最新變化,還會(huì)看到導(dǎo)致變化的代碼的副作用。
我們需要了解以下有關(guān)讀寫volatile的內(nèi)容:
當(dāng)一個(gè)線程寫入一個(gè)volatile變量,另一個(gè)線程看到寫入,第一個(gè)線程會(huì)告訴第二個(gè)線程關(guān)于內(nèi)存變化的內(nèi)容,直到它執(zhí)行寫入該volatile變量。
在這里,線程2看到了線程1的內(nèi)容。
我們可以聲明 final 類型的volatile變量嗎?
如果一個(gè)變量是final的,我們不能改變它的值,volatile就是確保對(duì)其他線程可見的共享變量的更改。所以這是不允許的,并會(huì)導(dǎo)致編譯錯(cuò)誤。
為什么我們?cè)诓l(fā)編程中聲明long / double為volatile?
默認(rèn)情況下long/double的讀寫不是原子性的。非原子性的double/long寫操作會(huì)被當(dāng)做兩個(gè)寫入操作:分別寫入前32位和后32位。它可能會(huì)導(dǎo)致一個(gè)線程看到另一個(gè)線程寫入的64位值的前32位,而第二個(gè)線程看到來自另一個(gè)線程寫入的后32位。讀寫volatile的long/double類型變量總是原子性的。
Volatile vs Atomic類
public class MyApp { private volatile int count = 0; public void upateVisitors() { ++count; //increment the visitors count } }
如果我們將count聲明為atomic,這段代碼可以正常運(yùn)行嗎?可以的,而且當(dāng)對(duì)變量進(jìn)行增加或減少操作時(shí),最好使用atomic類。AtomicInteger通常使用volatile或是CAS來實(shí)現(xiàn)線程安全。
想要了解更多開發(fā)技術(shù),面試教程以及互聯(lián)網(wǎng)公司內(nèi)推,歡迎關(guān)注我的微信公眾號(hào)!將會(huì)不定期的發(fā)放福利哦~
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/68804.html
摘要:簡(jiǎn)介從創(chuàng)建以來,就支持核心的并發(fā)概念如線程和鎖。這篇文章會(huì)幫助從事多線程編程的開發(fā)人員理解核心的并發(fā)概念以及如何使用它們。請(qǐng)求操作系統(tǒng)互斥,并讓操作系統(tǒng)調(diào)度程序處理線程停放和喚醒。 簡(jiǎn)介 從創(chuàng)建以來,JAVA就支持核心的并發(fā)概念如線程和鎖。這篇文章會(huì)幫助從事多線程編程的JAVA開發(fā)人員理解核心的并發(fā)概念以及如何使用它們。 (博主將在其中加上自己的理解以及自己想出的例子作為補(bǔ)充) 概念 ...
摘要:否則它就會(huì)用新的值替代當(dāng)前值。在這種情況下,鎖可能會(huì)優(yōu)于原子變量,但在實(shí)際的爭(zhēng)用級(jí)別中,原子變量的性能優(yōu)于鎖。在中引入了另外一個(gè)構(gòu)件。 題目要求 在我們深入了解CAS(Compare And Swap)策略以及它是如何在AtomicInteger這樣的原子構(gòu)造器中使用的,首先來看一下這段代碼: public class MyApp { private volatile int ...
摘要:內(nèi)存模型是圍繞著在并發(fā)過程中如何處理原子性可見性和有序性這個(gè)特征來建立的,我們來看下哪些操作實(shí)現(xiàn)了這個(gè)特性。可見性可見性是指當(dāng)一個(gè)線程修改了共享變量的值,其他線程能夠立即得知這個(gè)修改。 Java內(nèi)存模型是圍繞著在并發(fā)過程中如何處理原子性、可見性和有序性這3個(gè)特征來建立的,我們來看下哪些操作實(shí)現(xiàn)了這3個(gè)特性。 原子性(atomicity): 由Java內(nèi)存模型來直接保證原子性變量操作包括...
摘要:前半句是指線程內(nèi)表現(xiàn)為串行的語(yǔ)義,后半句是指指令重排序現(xiàn)象和工作內(nèi)存和主內(nèi)存同步延遲現(xiàn)象。關(guān)于內(nèi)存模型的講解請(qǐng)參考死磕同步系列之。目前國(guó)內(nèi)市面上的關(guān)于內(nèi)存屏障的講解基本不會(huì)超過這三篇文章,包括相關(guān)書籍中的介紹。問題 (1)volatile是如何保證可見性的? (2)volatile是如何禁止重排序的? (3)volatile的實(shí)現(xiàn)原理? (4)volatile的缺陷? 簡(jiǎn)介 volatile...
摘要:前半句是指線程內(nèi)表現(xiàn)為串行的語(yǔ)義,后半句是指指令重排序現(xiàn)象和工作內(nèi)存和主內(nèi)存同步延遲現(xiàn)象。關(guān)于內(nèi)存模型的講解請(qǐng)參考死磕同步系列之。目前國(guó)內(nèi)市面上的關(guān)于內(nèi)存屏障的講解基本不會(huì)超過這三篇文章,包括相關(guān)書籍中的介紹。問題 (1)volatile是如何保證可見性的? (2)volatile是如何禁止重排序的? (3)volatile的實(shí)現(xiàn)原理? (4)volatile的缺陷? 簡(jiǎn)介 volatile...
閱讀 1231·2021-11-25 09:43
閱讀 1984·2021-11-11 10:58
閱讀 1206·2021-11-08 13:18
閱讀 2705·2019-08-29 16:25
閱讀 3524·2019-08-29 12:51
閱讀 3321·2019-08-29 12:30
閱讀 763·2019-08-26 13:24
閱讀 3697·2019-08-26 10:38