面試官:有了解過指令重排嗎,什么是Happens-Before
重排序
首先,什么是重排序?計算機在執行過程中,為了提高性能,會對編譯器和編譯器做指令重排。
這么做為啥可以提高性能呢?
我們知道計算機在執行的時候都是一個個指令去執行,不同的指令可能操作的硬件不一樣,在執行的過程中可能會產生中斷,打個比方,兩個指令a和b他們操作的東西各不相同,如果加載a的時候停頓了,b就加載不到,但是實際上它們互補影響,我也可以先加載b在加載a,所以指令重排是減少停頓的一種方法,這樣大大提高了效率。
指令重排的方式
指令重排一般分為以下三種:
- 編譯器優化重新安排語句的執行順序。
- 指令并行重排利用指令級并行技術將多個指令并行執行,如果指令之前沒有數據依賴,處理器可以改變對應機器指令的執行順序。
- 內存系統重排由于處理使用緩存和讀寫緩沖區,所以它們是亂序的。
指令重排可以保證串行語義一致,但是沒有義務保證多線程間的語義也一致**。所以在多線程下,指令重排序可能會導致一些問題。
順序一致性模型
順序一致性模型是一個「理論參考模型」,內存模型在設計的時候都會以順序一致性內存模型作為參考。
數據競爭
我們知道在多線程情況下,同時讀寫一個變量會導致結果的不確定性,這就存在了數據競爭,相反的如果線程在同步情況下,就不存在數據競爭。
JMM對于同步的多線程情況下,程序執行可以保證順序一致性,同步包括了使用volatile、final、synchronized等關鍵字來實現「多線程下的同步」,這里的前提正確使用它們,如果使用不當,就不能保證
什么是順序一致性模型
我們在上節給大家講了Java的內存模型,提到了內存可見性的概念,順序一致性模型它的最終目的就是保證內存的可見性。
它主要有兩大特性:
- 一個線程中的所有操作必須按照程序的順序(代碼順序)來執行。
- 不管線程是否同步,所有線程保持單一的執行順序并且可見,且是原子性
JMM中同步的順序一致性
在JMM中,臨界區(同步方法或同步塊)的代碼可以發生重排,但對其它線程是無感知的,這樣既提高了執行效率又不影響最終結果
JMM中未同步的順序一致性
- JMM沒有保證未同步程序的執行結果與該程序在順序一致性中執行結果一致。
- JMM不保證單線程內的操作會按程序的順序執行(因為指令重排)。
- JMM不保證所有線程能看到一致的操作執行順序(因為不能保證所以操作立即可見)。
- JMM不保證對64位的long型和double型變量的寫操作具有原子性。
什么是happens-before
JMM提供了「happens-before規則」(JSR-133規范), 開發者可以遵循這種規范編寫程序,可以保證程序在JMM中具有強的內存可見性。JMM使用happens-before的概念來定制兩個操作之間的執行順序。這兩個操作可以在一個線程以內,也可以是不同的線程之間。因此,JMM可以通過happens-before關系向程序員提供跨線程的內存可見性保證。
happens-before關系的定義如下:
- 如果一個操作happens-before另一個操作,那么第一個操作的執行結果將對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前。
- 「兩個操作之間存在happens-before關系,并不意味著Java平臺的具體實現必須要按照happens-before關系指定的順序來執行。如果重排序之后的執行結果,與按happens-before關系來執行的結果一致,那么JMM也允許這樣的重排序。」
總之,「如果操作A happens-before操作B,那么操作A在內存上所做的操作對操作B都是可見的,不管它們在不在一個線程。」
在Java中,有以下天然的happens-before關系:
- 程序順序規則:一個線程中的每一個操作,happens-before于該線程中的任意后續操作。
- 監視器鎖規則:對一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖。
- volatile變量規則:對一個volatile域的寫,happens-before于任意后續對這個volatile域的讀。
- 傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C。
- start規則:ThreadA start happens-before ThreadB start
- join規則:如果線程A執行操作ThreadB.join()并成功返回,那么線程B中的任意操作happens-before于線程A從ThreadB.join()操作成功返回。
結束語
本節內容可能不像之前那么好理解,比較抽象,所以本文也有不足的地方,大家自己可以多查查一些資料,綜合理解。下一節,帶大家深入學習一下Java的volatile。