看Java內(nèi)存模型(JMM, Java Memory Model)時,總有一個困惑。關于線程、主存(main memory)、工作內(nèi)存(working memory),我都能找到實際映射的硬件:線程可能對應著一個內(nèi)核線程,主存對應著內(nèi)存,而工作內(nèi)存則涵蓋了寫緩沖區(qū)、緩存(cache)、寄存器等一系列為了提高數(shù)據(jù)存取效率的暫存區(qū)域。但是,一提到happens-before原則,就讓人有點“丈二和尚摸不著頭腦”。這個涵蓋了整個JMM中可見性原則的規(guī)則,究竟如何理解,把我個人一些理解記錄下來。
創(chuàng)新互聯(lián)-專業(yè)網(wǎng)站定制、快速模板網(wǎng)站建設、高性價比龍湖網(wǎng)站開發(fā)、企業(yè)建站全套包干低至880元,成熟完善的模板庫,直接使用。一站式龍湖網(wǎng)站制作公司更省心,省錢,快速模板網(wǎng)站建設找我們,業(yè)務覆蓋龍湖地區(qū)。費用合理售后完善,十載實體公司更值得信賴。
兩個操作間具有happens-before關系,并不意味著前一個操作必須要在后一個操作之前執(zhí)行。happens-before僅僅要求前一個操作對后一個操作可見。
這個說法我先后在好幾本書中都看到過。也就是說,happens-before原則和一般意義上的時間先后是不同的。那究竟是什么呢?一步步來看。
順序一致性內(nèi)存模型
我們先來看一個理想化的模型:順序一致性(Sequentially Consistent)內(nèi)存模型。在這個模型里,所有操作按程序的順序來執(zhí)行,并且每一個操作都是原子的,且立即對所有線程可見。
這個系統(tǒng)中同一時間只有一個線程能讀或?qū)憙?nèi)存。也就是說,這個系統(tǒng)里的每兩個指令之間,都嚴格按執(zhí)行的先后,具有著happens-before關系。所有的線程,都能夠看到一致的全局指令執(zhí)行視圖。如果將總線1看做是線程和內(nèi)存之間的通道,那么順序一致性模型就相當于在所有讀/寫內(nèi)存的操作時,鎖住總線。
特別注意一點,順序一致性模型,不代表多線程沒有同步問題,只是每個操作之間不存在同步問題,如果你的操作是多個操作的集合體,照樣不能安全工作。圖中所示的是常見的自增操作,兩個線程都有同樣的執(zhí)行視圖:1->2->3->4->5->6。然而,線程A的寫結(jié)果,依然被線程B所覆蓋了。A線程讀寫固然對B線程立即可見,但是由于5/6的寫操作對于內(nèi)存的影響依賴于1/2的讀操作,所以對于多線程仍然存在問題。
顯然,順序一致性模型是一種犧牲并行度、換取多線程對共享內(nèi)存的可見性的一種理想模型。從JMM實現(xiàn)volatile以及synchronized的內(nèi)存語義的方式,正是鎖住總線或者說鎖住線程自身存儲(指working memory)。
Java內(nèi)存模型
關于Java內(nèi)存模型的書籍文章,汗牛充棟,想必大家也都有自己的理解。那就僅僅由上面的順序一致性模型來引出JMM,看看具體區(qū)別在哪。
可以看出,工作內(nèi)存是一個明顯區(qū)別于順序一致性內(nèi)存模型的地方。事實上,造成可見性問題的根源之一,就在于這個工作內(nèi)存(強調(diào)一下,包括緩存、寫緩沖和寄存器等等)。工作內(nèi)存使得每個線程都有了自己的私有存儲,大部分時間對數(shù)據(jù)的存取工作都在這個區(qū)域完成。但是我們寫一個數(shù)據(jù),是直到數(shù)據(jù)寫到主存中才算真正完成。實際上每個線程維護了一個副本,所有線程都在自己的工作內(nèi)存中不斷地讀/寫一個共享內(nèi)存中的數(shù)據(jù)的副本。單線程情況下,這個副本不會造成任何問題;但一旦到多線程,有一個線程將變量寫到主存,其他線程卻不知道,其他線程的副本就都過期。比如,由于工作內(nèi)存的存在,程序員寫的一段代碼,寫一個普通的共享變量,其可能先被寫到緩沖區(qū),那指令完成的時間就被推遲了,實際表現(xiàn)也就是我們常說的“指令重排序”(這實際上是內(nèi)存模型層面的重排序,重排序還可能是編譯器、機器指令層級上的亂序)。
因此,在Java內(nèi)存模型中,每個線程不再像順序一致性模型中那樣有確定的指令執(zhí)行視圖,一個指令可能被重排了。從一個線程的角度看,其他線程(甚至是這個線程本身)執(zhí)行的指令順序有多種可能性,也就是說,一個線程的執(zhí)行結(jié)果對其他線程的可見性無法保證。
總結(jié)一下導致可見性問題的原因:
1.數(shù)據(jù)的寫無法及時通知到別的線程,如寫緩沖區(qū)的引入
2.線程不能及時讀到其他線程對共享變量的修改,如緩存的使用
3.各種層級上對指令的重排序,導致指令執(zhí)行的順序無法確定
所以要解決可見性問題,本質(zhì)是要讓線程對共享變量的修改,及時同步到其他線程。我們所使用的硬件架構(gòu)下,不具備順序一致性內(nèi)存模型的全局一致的指令執(zhí)行順序,討論指令執(zhí)行的時間先后并不存在意義或者說根本沒辦法確定時間上的先后??梢钥纯聪旅娉绦?,每個線程中的flag副本會在多久后被更新呢?答案是:無法確定,看線程何時刷新自己的工作內(nèi)存。
public class testVisibility { public static boolean flag = false; public static void main(String[] args) { ListthdList = new ArrayList (); for(int i = 0; i < 10; i++) { Thread t = new Thread(new Runnable(){ public void run() { while (true) { if (flag) { // 多運行幾次,可能并不會打印出來也可能會打印出來 // 如果不打印,則表示Thread看到的仍然是工作內(nèi)存中的flag // 可以嘗試將flag變成volatile再運行幾次看看 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í)行的先后,也不需要討論,我們實際只想知道某線程的操作對另一個線程是否可見,于是就規(guī)定了happens-before這個可見性原則,程序員可以基于這個原則進行可見性的判斷。
volatile變量
volatile就是一個踐行happens-before的關鍵字。看以下對volatile的描述,就不難知道,happens-before指的是線程接收其他線程修改共享變量的消息與該線程讀取共享變量的先后關系。大家可以再細想一下,如果沒有happens-before原則,豈不是相當于一個線程讀取自己的共享變量副本時,其他線程修改這個變量的消息還沒有同步過來?這就是可見性問題。
volatile變量規(guī)則:對一個volatile的寫,happens-before于任意后續(xù)對這個volatile變量的讀。
線程A寫一個volatile變量,實質(zhì)上是線程A向接下來要獲取這個鎖的某個線程發(fā)出了(線程A對共享變量修改的)消息。
線程B讀一個volatile變量,實質(zhì)上是線程B接收了之前某個線程發(fā)出的(對共享變量所做修改的)消息。
線程A寫一個volatile變量,隨后線程B讀這個變量,這個過程實質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息。
其實仔細看看volatile的實現(xiàn)方式,實際上就是限制了重排序的范圍——加入內(nèi)存屏障(Memory Barrier or Memory Fence)。也即是說,允許指令執(zhí)行的時間先后順序在一定范圍內(nèi)發(fā)生變化,而這個范圍就是根據(jù)happens-before原則來規(guī)定。內(nèi)存屏障概括起來有兩個功能:
1.使寫緩沖區(qū)的內(nèi)容刷新到內(nèi)存,保證對其他線程/CPU可見
2.禁止讀寫操作的越過內(nèi)存屏障進行重排序
而這上述功能組合起來,就完成上面所說的happens-before所表達的線程通信過程。
每個volatile寫操作的前面插入一個StoreStore屏障
每個volatile寫操作的后面插入一個StoreLoad屏障
每個volatile讀操作的后面插入一個LoadLoad屏障
每個volatile讀操作的后面插入一個LoadStore屏障
關于內(nèi)存屏障的種類,這里不是研究的重點。一直困擾我的是,在多處理器系統(tǒng)下,這個屏障如何能跨越處理器來阻止操作執(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)換,可以參看總線事務(Bus transaction)的概念。當CPU要與內(nèi)存進行數(shù)據(jù)交換的時候,實際上總線會同步數(shù)據(jù)交換操作,同一時刻只能有一個CPU進行讀/寫內(nèi)存,所以我們所看到的多處理器并行,并行的是CPU的計算資源。在總線看來,對于存儲的讀寫操作就是串行的,是按照一定順序的。這也就是為什么一個內(nèi)存屏障能夠跨越處理器去限制讀寫、去完成通信。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持創(chuàng)新互聯(lián)。