Java內存模型(JSR - 133)都解決了哪些問題?

 

c01cd6cd162d01e13431a3913169ac18.jpeg

究竟什麼是內存模型?

在多處理系統中,每一個 CPU 一般都包含一層或者多層內存緩存,這樣設計的緣由是爲了加快數據訪問速度(由於數據會更靠近處理器) 而且可以減小共享內存總線上的流量(由於能夠知足許多內存操做)來提升性能。內存緩存可以極大的提升性能。程序員

可是同時,這種設計方式也帶來了許多挑戰。面試

好比,當兩個 CPU 同時對同一內存位置進行操做時會發生什麼?在什麼狀況下這兩個 CPU 會看到同一個內存值?編程

如今,內存模型登場了!!!在處理器層面,內存模型明肯定義了其餘處理器的寫入是如何對當前處理器保持可見的,以及當前處理器寫入內存的值是如何使其餘處理器可見的,這種特性被稱爲可見性,這是官方定義的一種說法。數組

然而,可見性也分爲強可見性弱可見性,強可見性說的是任何 CPU 都可以看到指定內存位置具備相同的值;弱可見性說的是須要一種被稱爲內存屏障的特殊指令來刷新緩存或者使本地處理器緩存無效,才能看到其餘 CPU 對指定內存位置寫入的值,寫入後的值就是內存值。這些特殊的內存屏障是被封裝以後的,咱們不研究源碼的話是不知道內存屏障這個概念的。緩存

內存模型還規定了另一種特性,這種特性可以使編譯器對代碼進行從新排序(其實從新排序不僅是編譯器所具備的特性),這種特性被稱爲有序性。若是兩行代碼彼此沒有相關性,那麼編譯器是可以改變這兩行代碼的編譯順序的,只要代碼不會改變程序的語義,那麼編譯器就會這樣作。安全

咱們上面剛提到了,從新排序不僅是編譯器所特有的功能,編譯器的這種重排序只是一種靜態重排序,其實在運行時或者硬件執行指令的過程當中也會發生重排序,重排序是一種提升程序運行效率的一種方式。多線程

好比下面這段代碼架構

Class Reordering {
  int x = 0, y = 0;
  public void writer() {
    x = 1;
    y = 2;
  }

  public void reader() {
    int r1 = y;
    int r2 = x;
  }
}

當兩個線程並行執行上面這段代碼時,可能會發生重排序現象,由於 x 、 y 是兩個互不相關的變量,因此當線程一執行到 writer 中時,發生重排序,y = 2 先被編譯,而後線程切換,執行 r1 的寫入,緊接着執行 r2 的寫入,注意此時 x 的值是 0 ,由於 x = 1 沒有編譯。這時候線程切換到 writer ,編譯 x = 1,因此最後的值爲 r1 = 2,r2 = 0,這就是重排序可能致使的後果。併發

因此 Java 內存模型爲咱們帶來了什麼?app

Java 內存模型描述了多線程中哪些行爲是合法的,以及線程之間是如何經過內存進行交互的。Java 內存模型提供了兩種特性,即變量之間的可見性和有序性,這些特性是須要咱們在平常開發中所注意到的點。Java 中也提供了一些關鍵字好比 volatile、final 和 synchronized 來幫助咱們應對 Java 內存模型帶來的問題,同時 Java 內存模型也定義了 volatile 和 synchronized 的行爲。

其餘語言,好比 C++ 會有內存模型嗎?

其餘語言好比 C 和 C++ 在設計時並未直接支持多線程,這些語言針對編譯器和硬件發生的重排序是依靠線程庫(好比 pthread )、所使用的編譯器以及運行代碼的平臺提供的保證。

JSR - 133 是關於啥的?

在 1997 年,在此時 Java 版本中的內存模型中發現了幾個嚴重的缺陷,這個缺陷常常會出現詭異的問題,好比字段的值常常會發生改變,而且很是容易削弱編譯器的優化能力。

因此,Java 提出了一項雄心勃勃的暢想:合併內存模型,這是編程語言規範第一次嘗試合併一個內存模型,這個模型可以爲跨各類架構的併發性提供一致的語義,可是實際操做起來要比暢想困難不少。

最終,JSR-133 爲 Java 語言定義了一個新的內存模型,它修復了早期內存模型的缺陷。

因此,咱們說的 JSR - 133 是關於內存模型的一種規範和定義。

JSR - 133 的設計目標主要包括:

  • 保留 Java 現有的安全性保證,好比類型安全,並增強其餘安全性保證,好比線程觀察到的每一個變量的值都必須是某個線程對變量進行修改以後的。
  • 程序的同步語義應該儘量簡單和直觀。
  • 將多線程如何交互的細節交給程序員進行處理。
  • 在普遍、流行的硬件架構上設計正確、高性能的 JVM 實現。
  • 應提供初始化安全的保證,若是一個對象被正確構造後,那麼全部看到對象構造的線程都可以看到構造函數中設置其最終字段的值,而不用進行任何的同步操做。
  • 對現有的代碼影響要儘量的小。
重排序是什麼?

在不少狀況下,訪問程序變量,好比對象實例字段、類靜態字段和數組元素的執行順序與程序員編寫的程序指定的執行順序不一樣。編譯器能夠以優化的名義任意調整指令的執行順序。在這種狀況下,數據能夠按照不一樣於程序指定的順序在寄存器、處理器緩存和內存之間移動。

有許多潛在的從新排序來源,例如編譯器、JIT(即時編譯)和緩存。

重排序是硬件、編譯器一塊兒製造出來的一種錯覺,在單線程程序中不會發生重排序的現象,重排序每每發生在未正確同步的多線程程序中。

舊的內存模型有什麼錯誤?

新內存模型的提出是爲了彌補舊內存模型的不足,因此舊內存模型有哪些不足,我相信讀者也能大體猜到了。

首先,舊的內存模型不容許發生重排序。再一點,舊的內存模型沒有保證 final 的真正 不可變性,這是一個很是使人大跌眼睛的結論,舊的內存模型沒有把 final 和其餘不用 final 修飾的字段區別對待,這也就意味着,String 並不是是真正不可變,這確實是一個很是嚴重的問題。

其次,舊的內存模型容許 volatile 寫入與非 volatile 讀取和寫入從新排序,這與大多數開發人員對 volatile 的直覺不一致,所以引發了混亂。

什麼是不正確同步?

當咱們討論不正確同步的時候,咱們指的是任何代碼

  • 一個線程對一個變量執行寫操做,
  • 另外一個線程讀取了相同的變量,
  • 而且讀寫之間並無正確的同步

當違反這些規則時,咱們說在這個變量上發生了數據競爭現象。 具備數據競爭現象的程序是不正確同步的程序。

同步(synchronization)都作了哪些事情?

同步有幾個方面,最容易理解的是互斥,也就是說一次只有一個線程能夠持有一個監視器(monitor),因此在 monitor 上的同步意味着一旦一個線程進入一個受 monitor 保護的同步代碼塊,其餘線程就不能進入受該 monitor 保護的塊直到第一個線程退出同步代碼塊。

可是同步不只僅只有互斥,它還有可見,同步可以確保線程在進入同步代碼塊以前和同步代碼塊執行期間,線程寫入內存的值對在同一 monitor 上同步的其餘線程可見。

在進入同步塊以前,會獲取 monitor ,它具備使本地處理器緩存失效的效果,以便變量將從主內存中從新讀取。 在退出一個同步代碼塊後,會釋放 monitor ,它具備將緩存刷新到主存的功能,以便其餘線程能夠看到該線程所寫入的值

新的內存模型語義在內存操做上面制定了一些特定的順序,這些內存操做包含(read、write、lock、unlock)和一些線程操做(start 、join),這些特定的順序保證了第一個動做在執行以前對第二個動做可見,這就是 happens-before 原則,這些特定的順序有

  • 線程中的每一個操做都 happens - before 按照程序定義的線程操做以前。
  • Monitor 中的每一個 unlock 操做都 happens-before 相同 monitor 的後續 lock 操做以前。
  • 對 volatile 字段的寫入都 happens-before 在每次後續讀取同一 volatile 變量以前。
  • 對線程的 start() 調用都 happens-before 在已啓動線程的任何操做以前。
  • 線程中的全部操做都 happens-before 在任何其餘線程從該線程上的 join() 成功返回以前。

須要注意很是重要的一點:兩個線程在同一個 monitor 之間的同步很是重要。並非線程 A 在對象 X 上同步時可見的全部內容在對象 Y 上同步後對線程 B 可見。釋放和獲取必須進行匹配(即,在同一個 monitor 上執行)纔能有正確的內存語義,不然就會發生數據競爭現象。

 

final 在新的 JMM 下是如何工做的?

經過上面的講述,你如今已經知道,final 在舊的 JMM 下是沒法正常工做的,在舊的 JMM 下,final 的語義就和普通的字段同樣,沒什麼其餘區別,可是在新的 JMM 下,final 的這種內存語義發生了質的改變,下面咱們就來探討一下 final 在新的 JMM 下是如何工做的。

對象的 final 字段在構造函數中設置,一旦對象被正確的構造出來,那麼在構造函數中的 final 的值將對其餘全部線程可見,無需進行同步操做。

什麼是正確的構造呢?

正確的構造意味着在構造的過程當中不容許對正在構造的對象的引用發生 逃逸,也就是說,不要將正在構造的對象的引用放在另一個線程可以看到它的地方。下面是一個正確構造的示例:

class FinalFieldExample {
  final int x;
  int y;
  static FinalFieldExample f;
  public FinalFieldExample() {
    x = 3;
    y = 4;
  }

  static void writer() {
    f = new FinalFieldExample();
  }

  static void reader() {
    if (f != null) {
      int i = f.x;
      int j = f.y;
    }
  }
}

執行讀取器的線程必定會看到 f.x 的值 3,由於它是 final 的。 不能保證看到 y 的值 4,由於它不是 final 的。 若是 FinalFieldExample 的構造函數以下所示:

public FinalFieldExample() { 
  x = 3;
  y = 4;
  // 錯誤的構造,可能會發生逃逸
  global.obj = this;
}

這樣就不會保證讀取 x 的值必定是 3 了。

這也就說是,若是在一個線程構造了一個不可變對象(即一個只包含 final 字段的對象)以後,你想要確保它被全部其餘線程正確地看到,一般仍然須要正確的使用同步。

 

新的內存模型修復了雙重檢查鎖的問題嗎?

也許咱們你們都見過多線程單例模式雙重檢查鎖的寫法,這是一種支持延遲初始化同時避免同步開銷的技巧。

class DoubleCheckSync{
 	private static DoubleCheckSync instance = null;
  public DoubleCheckSync getInstance() {
    if (instance == null) {
      synchronized (this) {
        if (instance == null)
          instance = new DoubleCheckSync();
      }
    }
    return instance;
  } 
}

這樣的代碼看起來在程序定義的順序上看起來很聰明,可是這段代碼卻有一個致命的問題:它不起做用

??????

雙重檢查鎖不起做用?

是的!

爲毛?

緣由就是初始化實例的寫入和對實例字段的寫入能夠由編譯器或緩存從新排序,看起來咱們可能讀取了初始化了 instance 對象,但其實你可能只是讀取了一個未初始化的 instance 對象。

有不少小夥伴認爲使用 volatile 可以解決這個問題,可是在 1.5 以前的 JVM 中,volatile 不能保證。在新的內存模型下,使用 volatile 會修復雙重檢查鎖定的問題,由於這樣在構造線程初始化 DoubleCheckSync 和返回其值之間將存在 happens-before 關係讀取它的線程。

最後

最近我整理了整套《JAVA核心知識點總結》,說實話 ,做爲一名Java程序員,不論你需不須要面試都應該好好看下這份資料。拿到手老是不虧的~個人很多粉絲也所以拿到騰訊字節快手等公司的Offer

Java進階之路羣,找管理員獲取哦-!

3318f3a472606f9908bb30a47c35ed85.png

5a192ddc84eca6a291d43ec1b2b63dfe.png

相關文章
相關標籤/搜索