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