深入淺出了解happens-before原則
看Java內(nèi)存模型(JMM, Java Memory Model)時(shí),總有一個(gè)困惑。關(guān)于線程、主存(main memory)、工作內(nèi)存(working memory),我都能找到實(shí)際映射的硬件:線程可能對(duì)應(yīng)著一個(gè)內(nèi)核線程,主存對(duì)應(yīng)著內(nèi)存,而工作內(nèi)存則涵蓋了寫緩沖區(qū)、緩存(cache)、寄存器等一系列為了提高數(shù)據(jù)存取效率的暫存區(qū)域。但是,一提到happens-before原則,就讓人有點(diǎn)“丈二和尚摸不著頭腦”。這個(gè)涵蓋了整個(gè)JMM中可見性原則的規(guī)則,究竟如何理解,把我個(gè)人一些理解記錄下來。
兩個(gè)操作間具有happens-before關(guān)系,并不意味著前一個(gè)操作必須要在后一個(gè)操作之前執(zhí)行。happens-before僅僅要求前一個(gè)操作對(duì)后一個(gè)操作可見。
這個(gè)說法我先后在好幾本書中都看到過。也就是說,happens-before原則和一般意義上的時(shí)間先后是不同的。那究竟是什么呢?一步步來看。
順序一致性內(nèi)存模型
我們先來看一個(gè)理想化的模型:順序一致性(Sequentially Consistent)內(nèi)存模型。在這個(gè)模型里,所有操作按程序的順序來執(zhí)行,并且每一個(gè)操作都是原子的,且立即對(duì)所有線程可見。
這個(gè)系統(tǒng)中同一時(shí)間只有一個(gè)線程能讀或?qū)憙?nèi)存。也就是說,這個(gè)系統(tǒng)里的每兩個(gè)指令之間,都嚴(yán)格按執(zhí)行的先后,具有著happens-before關(guān)系。所有的線程,都能夠看到一致的全局指令執(zhí)行視圖。如果將總線1看做是線程和內(nèi)存之間的通道,那么順序一致性模型就相當(dāng)于在所有讀/寫內(nèi)存的操作時(shí),鎖住總線。
特別注意一點(diǎn),順序一致性模型,不代表多線程沒有同步問題,只是每個(gè)操作之間不存在同步問題,如果你的操作是多個(gè)操作的集合體,照樣不能安全工作。圖中所示的是常見的自增操作,兩個(gè)線程都有同樣的執(zhí)行視圖:1->2->3->4->5->6。然而,線程A的寫結(jié)果,依然被線程B所覆蓋了。A線程讀寫固然對(duì)B線程立即可見,但是由于5/6的寫操作對(duì)于內(nèi)存的影響依賴于1/2的讀操作,所以對(duì)于多線程仍然存在問題。
顯然,順序一致性模型是一種犧牲并行度、換取多線程對(duì)共享內(nèi)存的可見性的一種理想模型。從JMM實(shí)現(xiàn)volatile以及synchronized的內(nèi)存語義的方式,正是鎖住總線或者說鎖住線程自身存儲(chǔ)(指working memory)。
Java內(nèi)存模型
關(guān)于Java內(nèi)存模型的書籍文章,汗牛充棟,想必大家也都有自己的理解。那就僅僅由上面的順序一致性模型來引出JMM,看看具體區(qū)別在哪。
可以看出,工作內(nèi)存是一個(gè)明顯區(qū)別于順序一致性內(nèi)存模型的地方。事實(shí)上,造成可見性問題的根源之一,就在于這個(gè)工作內(nèi)存(強(qiáng)調(diào)一下,包括緩存、寫緩沖和寄存器等等)。工作內(nèi)存使得每個(gè)線程都有了自己的私有存儲(chǔ),大部分時(shí)間對(duì)數(shù)據(jù)的存取工作都在這個(gè)區(qū)域完成。但是我們寫一個(gè)數(shù)據(jù),是直到數(shù)據(jù)寫到主存中才算真正完成。實(shí)際上每個(gè)線程維護(hù)了一個(gè)副本,所有線程都在自己的工作內(nèi)存中不斷地讀/寫一個(gè)共享內(nèi)存中的數(shù)據(jù)的副本。單線程情況下,這個(gè)副本不會(huì)造成任何問題;但一旦到多線程,有一個(gè)線程將變量寫到主存,其他線程卻不知道,其他線程的副本就都過期。比如,由于工作內(nèi)存的存在,程序員寫的一段代碼,寫一個(gè)普通的共享變量,其可能先被寫到緩沖區(qū),那指令完成的時(shí)間就被推遲了,實(shí)際表現(xiàn)也就是我們常說的“指令重排序”(這實(shí)際上是內(nèi)存模型層面的重排序,重排序還可能是編譯器、機(jī)器指令層級(jí)上的亂序)。
因此,在Java內(nèi)存模型中,每個(gè)線程不再像順序一致性模型中那樣有確定的指令執(zhí)行視圖,一個(gè)指令可能被重排了。從一個(gè)線程的角度看,其他線程(甚至是這個(gè)線程本身)執(zhí)行的指令順序有多種可能性,也就是說,一個(gè)線程的執(zhí)行結(jié)果對(duì)其他線程的可見性無法保證。
總結(jié)一下導(dǎo)致可見性問題的原因:
1.數(shù)據(jù)的寫無法及時(shí)通知到別的線程,如寫緩沖區(qū)的引入
2.線程不能及時(shí)讀到其他線程對(duì)共享變量的修改,如緩存的使用
3.各種層級(jí)上對(duì)指令的重排序,導(dǎo)致指令執(zhí)行的順序無法確定
所以要解決可見性問題,本質(zhì)是要讓線程對(duì)共享變量的修改,及時(shí)同步到其他線程。我們所使用的硬件架構(gòu)下,不具備順序一致性內(nèi)存模型的全局一致的指令執(zhí)行順序,討論指令執(zhí)行的時(shí)間先后并不存在意義或者說根本沒辦法確定時(shí)間上的先后??梢钥纯聪旅娉绦颍總€(gè)線程中的flag副本會(huì)在多久后被更新呢?答案是:無法確定,看線程何時(shí)刷新自己的工作內(nèi)存。
public class testVisibility { public static boolean flag = false; public static void main(String[] args) { List<Thread> thdList = new ArrayList<Thread>(); for(int i = 0; i < 10; i++) { Thread t = new Thread(new Runnable(){ public void run() { while (true) { if (flag) { // 多運(yùn)行幾次,可能并不會(huì)打印出來也可能會(huì)打印出來 // 如果不打印,則表示Thread看到的仍然是工作內(nèi)存中的flag // 可以嘗試將flag變成volatile再運(yùn)行幾次看看 System.out.println(Thread.currentThread().getId() + " is true now"); } } } }); t.start(); thdList.add(t); } flag = true; System.out.println("set flag true"); // 等待線程執(zhí)行完畢 try { for (Thread t : thdList) { t.join(); } } catch (Exception e) { } } }
那么既然我們無法討論指令執(zhí)行的先后,也不需要討論,我們實(shí)際只想知道某線程的操作對(duì)另一個(gè)線程是否可見,于是就規(guī)定了happens-before這個(gè)可見性原則,程序員可以基于這個(gè)原則進(jìn)行可見性的判斷。
volatile變量
volatile就是一個(gè)踐行happens-before的關(guān)鍵字??匆韵聦?duì)volatile的描述,就不難知道,happens-before指的是線程接收其他線程修改共享變量的消息與該線程讀取共享變量的先后關(guān)系。大家可以再細(xì)想一下,如果沒有happens-before原則,豈不是相當(dāng)于一個(gè)線程讀取自己的共享變量副本時(shí),其他線程修改這個(gè)變量的消息還沒有同步過來?這就是可見性問題。
volatile變量規(guī)則:對(duì)一個(gè)volatile的寫,happens-before于任意后續(xù)對(duì)這個(gè)volatile變量的讀。
線程A寫一個(gè)volatile變量,實(shí)質(zhì)上是線程A向接下來要獲取這個(gè)鎖的某個(gè)線程發(fā)出了(線程A對(duì)共享變量修改的)消息。
線程B讀一個(gè)volatile變量,實(shí)質(zhì)上是線程B接收了之前某個(gè)線程發(fā)出的(對(duì)共享變量所做修改的)消息。
線程A寫一個(gè)volatile變量,隨后線程B讀這個(gè)變量,這個(gè)過程實(shí)質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息。
其實(shí)仔細(xì)看看volatile的實(shí)現(xiàn)方式,實(shí)際上就是限制了重排序的范圍——加入內(nèi)存屏障(Memory Barrier or Memory Fence)。也即是說,允許指令執(zhí)行的時(shí)間先后順序在一定范圍內(nèi)發(fā)生變化,而這個(gè)范圍就是根據(jù)happens-before原則來規(guī)定。內(nèi)存屏障概括起來有兩個(gè)功能:
1.使寫緩沖區(qū)的內(nèi)容刷新到內(nèi)存,保證對(duì)其他線程/CPU可見
2.禁止讀寫操作的越過內(nèi)存屏障進(jìn)行重排序
而這上述功能組合起來,就完成上面所說的happens-before所表達(dá)的線程通信過程。
每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障
每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障
每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障
每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障
關(guān)于內(nèi)存屏障的種類,這里不是研究的重點(diǎn)。一直困擾我的是,在多處理器系統(tǒng)下,這個(gè)屏障如何能跨越處理器來阻止操作執(zhí)行的順序呢?比如下面的讀寫操作:
public static volatile int race = 0; // Thread A public static void save(int src) { race = src; } // Thread B public static int load() { return race; }
這就要提到從操作系統(tǒng)到硬件層面的觀念轉(zhuǎn)換,可以參看總線事務(wù)(Bus transaction)的概念。當(dāng)CPU要與內(nèi)存進(jìn)行數(shù)據(jù)交換的時(shí)候,實(shí)際上總線會(huì)同步數(shù)據(jù)交換操作,同一時(shí)刻只能有一個(gè)CPU進(jìn)行讀/寫內(nèi)存,所以我們所看到的多處理器并行,并行的是CPU的計(jì)算資源。在總線看來,對(duì)于存儲(chǔ)的讀寫操作就是串行的,是按照一定順序的。這也就是為什么一個(gè)內(nèi)存屏障能夠跨越處理器去限制讀寫、去完成通信。
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Java內(nèi)存模型(JMM)及happens-before原理
- Java內(nèi)存模型之happens-before概念詳解
- JAVA內(nèi)存模型和Happens-Before規(guī)則知識(shí)點(diǎn)講解
- 深入理解happens-before和as-if-serial語義
- volatile與happens-before的關(guān)系與內(nèi)存一致性錯(cuò)誤
- 簡單易懂講解happens-before原則
- Java內(nèi)存之happens-before和重排序
- 淺談Java內(nèi)存模型之happens-before
- JMM核心概念之Happens-before原則
相關(guān)文章
mybatis 自定義實(shí)現(xiàn)攔截器插件Interceptor示例
這篇文章主要介紹了mybatis 自定義實(shí)現(xiàn)攔截器插件Interceptor,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10Java使用訪問者模式解決公司層級(jí)結(jié)構(gòu)圖問題詳解
這篇文章主要介紹了Java使用訪問者模式解決公司層級(jí)結(jié)構(gòu)圖問題,結(jié)合實(shí)例形式分析了訪問者模式的概念、原理及Java使用訪問者模式解決公司曾經(jīng)結(jié)構(gòu)圖問題的相關(guān)操作技巧與注意事項(xiàng),需要的朋友可以參考下2018-04-04Java+Eclipse+Selenium環(huán)境搭建的方法步驟
這篇文章主要介紹了Java+Eclipse+Selenium環(huán)境搭建的方法步驟,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-06-06JavaWeb中使用JavaMail實(shí)現(xiàn)發(fā)送郵件功能實(shí)例詳解
這篇文章主要介紹了JavaWeb中使用JavaMail實(shí)現(xiàn)發(fā)送郵件功能的實(shí)例代碼,非常不錯(cuò)具有參考借鑒價(jià)值,感興趣的朋友一起看看吧2016-05-05