溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

深入淺出了解happens-before原則

發(fā)布時(shí)間:2020-10-18 20:58:55 來(lái)源:腳本之家 閱讀:198 作者:zdxiq000 欄目:編程語(yǔ)言

看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)存則涵蓋了寫(xiě)緩沖區(qū)、緩存(cache)、寄存器等一系列為了提高數(shù)據(jù)存取效率的暫存區(qū)域。但是,一提到happens-before原則,就讓人有點(diǎn)“丈二和尚摸不著頭腦”。這個(gè)涵蓋了整個(gè)JMM中可見(jiàn)性原則的規(guī)則,究竟如何理解,把我個(gè)人一些理解記錄下來(lái)。

兩個(gè)操作間具有happens-before關(guān)系,并不意味著前一個(gè)操作必須要在后一個(gè)操作之前執(zhí)行。happens-before僅僅要求前一個(gè)操作對(duì)后一個(gè)操作可見(jiàn)。

這個(gè)說(shuō)法我先后在好幾本書(shū)中都看到過(guò)。也就是說(shuō),happens-before原則和一般意義上的時(shí)間先后是不同的。那究竟是什么呢?一步步來(lái)看。

順序一致性內(nèi)存模型

我們先來(lái)看一個(gè)理想化的模型:順序一致性(Sequentially Consistent)內(nèi)存模型。在這個(gè)模型里,所有操作按程序的順序來(lái)執(zhí)行,并且每一個(gè)操作都是原子的,且立即對(duì)所有線程可見(jiàn)。 
 深入淺出了解happens-before原則

這個(gè)系統(tǒng)中同一時(shí)間只有一個(gè)線程能讀或?qū)憙?nèi)存。也就是說(shuō),這個(gè)系統(tǒng)里的每?jī)蓚€(gè)指令之間,都嚴(yán)格按執(zhí)行的先后,具有著happens-before關(guān)系。所有的線程,都能夠看到一致的全局指令執(zhí)行視圖。如果將總線1看做是線程和內(nèi)存之間的通道,那么順序一致性模型就相當(dāng)于在所有讀/寫(xiě)內(nèi)存的操作時(shí),鎖住總線。

特別注意一點(diǎn),順序一致性模型,不代表多線程沒(méi)有同步問(wèn)題,只是每個(gè)操作之間不存在同步問(wèn)題,如果你的操作是多個(gè)操作的集合體,照樣不能安全工作。圖中所示的是常見(jiàn)的自增操作,兩個(gè)線程都有同樣的執(zhí)行視圖:1->2->3->4->5->6。然而,線程A的寫(xiě)結(jié)果,依然被線程B所覆蓋了。A線程讀寫(xiě)固然對(duì)B線程立即可見(jiàn),但是由于5/6的寫(xiě)操作對(duì)于內(nèi)存的影響依賴于1/2的讀操作,所以對(duì)于多線程仍然存在問(wèn)題。

深入淺出了解happens-before原則

顯然,順序一致性模型是一種犧牲并行度、換取多線程對(duì)共享內(nèi)存的可見(jiàn)性的一種理想模型。從JMM實(shí)現(xiàn)volatile以及synchronized的內(nèi)存語(yǔ)義的方式,正是鎖住總線或者說(shuō)鎖住線程自身存儲(chǔ)(指working memory)。

Java內(nèi)存模型

關(guān)于Java內(nèi)存模型的書(shū)籍文章,汗牛充棟,想必大家也都有自己的理解。那就僅僅由上面的順序一致性模型來(lái)引出JMM,看看具體區(qū)別在哪。

深入淺出了解happens-before原則

可以看出,工作內(nèi)存是一個(gè)明顯區(qū)別于順序一致性內(nèi)存模型的地方。事實(shí)上,造成可見(jiàn)性問(wèn)題的根源之一,就在于這個(gè)工作內(nèi)存(強(qiáng)調(diào)一下,包括緩存、寫(xiě)緩沖和寄存器等等)。工作內(nèi)存使得每個(gè)線程都有了自己的私有存儲(chǔ),大部分時(shí)間對(duì)數(shù)據(jù)的存取工作都在這個(gè)區(qū)域完成。但是我們寫(xiě)一個(gè)數(shù)據(jù),是直到數(shù)據(jù)寫(xiě)到主存中才算真正完成。實(shí)際上每個(gè)線程維護(hù)了一個(gè)副本,所有線程都在自己的工作內(nèi)存中不斷地讀/寫(xiě)一個(gè)共享內(nèi)存中的數(shù)據(jù)的副本。單線程情況下,這個(gè)副本不會(huì)造成任何問(wèn)題;但一旦到多線程,有一個(gè)線程將變量寫(xiě)到主存,其他線程卻不知道,其他線程的副本就都過(guò)期。比如,由于工作內(nèi)存的存在,程序員寫(xiě)的一段代碼,寫(xiě)一個(gè)普通的共享變量,其可能先被寫(xiě)到緩沖區(qū),那指令完成的時(shí)間就被推遲了,實(shí)際表現(xiàn)也就是我們常說(shuō)的“指令重排序”(這實(shí)際上是內(nèi)存模型層面的重排序,重排序還可能是編譯器、機(jī)器指令層級(jí)上的亂序)。

因此,在Java內(nèi)存模型中,每個(gè)線程不再像順序一致性模型中那樣有確定的指令執(zhí)行視圖,一個(gè)指令可能被重排了。從一個(gè)線程的角度看,其他線程(甚至是這個(gè)線程本身)執(zhí)行的指令順序有多種可能性,也就是說(shuō),一個(gè)線程的執(zhí)行結(jié)果對(duì)其他線程的可見(jiàn)性無(wú)法保證。

總結(jié)一下導(dǎo)致可見(jiàn)性問(wèn)題的原因:

1.數(shù)據(jù)的寫(xiě)無(wú)法及時(shí)通知到別的線程,如寫(xiě)緩沖區(qū)的引入
2.線程不能及時(shí)讀到其他線程對(duì)共享變量的修改,如緩存的使用
3.各種層級(jí)上對(duì)指令的重排序,導(dǎo)致指令執(zhí)行的順序無(wú)法確定

所以要解決可見(jiàn)性問(wèn)題,本質(zhì)是要讓線程對(duì)共享變量的修改,及時(shí)同步到其他線程。我們所使用的硬件架構(gòu)下,不具備順序一致性內(nèi)存模型的全局一致的指令執(zhí)行順序,討論指令執(zhí)行的時(shí)間先后并不存在意義或者說(shuō)根本沒(méi)辦法確定時(shí)間上的先后。可以看看下面程序,每個(gè)線程中的flag副本會(huì)在多久后被更新呢?答案是:無(wú)法確定,看線程何時(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ì)打印出來(lái)也可能會(huì)打印出來(lái)
       // 如果不打印,則表示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) {

  }
 }
}

那么既然我們無(wú)法討論指令執(zhí)行的先后,也不需要討論,我們實(shí)際只想知道某線程的操作對(duì)另一個(gè)線程是否可見(jiàn),于是就規(guī)定了happens-before這個(gè)可見(jiàn)性原則,程序員可以基于這個(gè)原則進(jìn)行可見(jiàn)性的判斷。

volatile變量

volatile就是一個(gè)踐行happens-before的關(guān)鍵字??匆韵聦?duì)volatile的描述,就不難知道,happens-before指的是線程接收其他線程修改共享變量的消息與該線程讀取共享變量的先后關(guān)系。大家可以再細(xì)想一下,如果沒(méi)有happens-before原則,豈不是相當(dāng)于一個(gè)線程讀取自己的共享變量副本時(shí),其他線程修改這個(gè)變量的消息還沒(méi)有同步過(guò)來(lái)?這就是可見(jiàn)性問(wèn)題。

volatile變量規(guī)則:對(duì)一個(gè)volatile的寫(xiě),happens-before于任意后續(xù)對(duì)這個(gè)volatile變量的讀。
線程A寫(xiě)一個(gè)volatile變量,實(shí)質(zhì)上是線程A向接下來(lái)要獲取這個(gè)鎖的某個(gè)線程發(fā)出了(線程A對(duì)共享變量修改的)消息。
線程B讀一個(gè)volatile變量,實(shí)質(zhì)上是線程B接收了之前某個(gè)線程發(fā)出的(對(duì)共享變量所做修改的)消息。
線程A寫(xiě)一個(gè)volatile變量,隨后線程B讀這個(gè)變量,這個(gè)過(guò)程實(shí)質(zhì)上是線程A通過(guò)主內(nèi)存向線程B發(fā)送消息。

其實(shí)仔細(xì)看看volatile的實(shí)現(xiàn)方式,實(shí)際上就是限制了重排序的范圍——加入內(nèi)存屏障(Memory Barrier or Memory Fence)。也即是說(shuō),允許指令執(zhí)行的時(shí)間先后順序在一定范圍內(nèi)發(fā)生變化,而這個(gè)范圍就是根據(jù)happens-before原則來(lái)規(guī)定。內(nèi)存屏障概括起來(lái)有兩個(gè)功能:

1.使寫(xiě)緩沖區(qū)的內(nèi)容刷新到內(nèi)存,保證對(duì)其他線程/CPU可見(jiàn)
2.禁止讀寫(xiě)操作的越過(guò)內(nèi)存屏障進(jìn)行重排序

而這上述功能組合起來(lái),就完成上面所說(shuō)的happens-before所表達(dá)的線程通信過(guò)程。

每個(gè)volatile寫(xiě)操作的前面插入一個(gè)StoreStore屏障
每個(gè)volatile寫(xiě)操作的后面插入一個(gè)StoreLoad屏障
每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障
每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障

關(guān)于內(nèi)存屏障的種類(lèi),這里不是研究的重點(diǎn)。一直困擾我的是,在多處理器系統(tǒng)下,這個(gè)屏障如何能跨越處理器來(lái)阻止操作執(zhí)行的順序呢?比如下面的讀寫(xiě)操作:

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)行讀/寫(xiě)內(nèi)存,所以我們所看到的多處理器并行,并行的是CPU的計(jì)算資源。在總線看來(lái),對(duì)于存儲(chǔ)的讀寫(xiě)操作就是串行的,是按照一定順序的。這也就是為什么一個(gè)內(nèi)存屏障能夠跨越處理器去限制讀寫(xiě)、去完成通信。

以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI