天天看點

Java 記憶體模型

在并發程式設計中,當多個線程同時通路同一個共享的可變變量時,會産生不确定的結果,是以要編寫線程安全的代碼,其本質上是對這些可變的共享變量的通路操作進行管理。導緻這種不确定結果的原因就是可見性、有序性和原子性問題,Java 為解決可見性和有序性問題引入了 Java 記憶體模型,使用互斥方案(其核心實作技術是鎖)來解決原子性問題。這篇先來看看解決可見性、有序性問題的 Java 記憶體模型(JMM)。

Java 記憶體模型在維基百科上的定義如下:

The Java memory model describes how threads in the Java programming language interact through memory. Together with the description of single-threaded execution of code, the memory model provides the semantics of the Java programming language.

記憶體模型限制的是共享變量,也就是存儲在堆記憶體中的變量,在 Java 語言中,所有的執行個體變量、靜态變量和數組元素都存儲在堆記憶體之中。而方法參數、異常處理參數這些局部變量存儲在方法棧幀之中,是以不會線上程之間共享,不會受到記憶體模型影響,也不存在記憶體可見性問題。

通常,線上程之間的通訊方式有共享記憶體和消息傳遞兩種,很明顯,Java 采用的是第一種即共享的記憶體模型,在共享的記憶體模型裡,多線程之間共享程式的公共狀态,通過讀-寫記憶體的方式來進行隐式通訊。

從抽象的角度來看,JMM 其實是定義了線程和主記憶體之間的關系,首先,多個線程之間的共享變量存儲在主記憶體之中,同時每個線程都有一個自己私有的本地記憶體,本地記憶體中存儲着該線程讀或寫共享變量的副本(注意:本地記憶體是 JMM 定義的抽象概念,實際上并不存在)。抽象模型如下圖所示:

Java 記憶體模型

在這個抽象的記憶體模型中,在兩個線程之間的通信(共享變量狀态變更)時,會進行如下兩個步驟:

線程 A 把在本地記憶體更新後的共享變量副本的值,重新整理到主記憶體中。

線程 B 在使用到該共享變量時,到主記憶體中去讀取線程 A 更新後的共享變量的值,并更新線程 B 本地記憶體的值。

JMM 本質上是在硬體(處理器)記憶體模型之上又做了一層抽象,使得應用開發人員隻需要了解 JMM 就可以編寫出正确的并發代碼,而無需過多了解硬體層面的記憶體模型。

在日常的程式開發中,為一些共享變量指派的場景會經常碰到,假設一個線程為整型共享變量 count 做指派操作(count = 9527;),此時就會有一個問題,其它讀取該共享變量的線程在什麼情況下擷取到的變量值為 9527 呢?如果缺少同步的話,會有很多因素導緻其它讀取該變量的線程無法立即甚至是永遠都無法看到該變量的最新值。

比如緩存就可能會改變寫入共享變量副本送出到主記憶體的次序,儲存在本地緩存的值,對于其它線程是不可見的;編譯器為了優化性能,有時候會改變程式中語句執行的先後順序,這些因素都有可能會導緻其它線程無法看到共享變量的最新值。

在文章開頭,提到了 JMM 主要是為了解決可見性和有序性問題,那麼首先就要先搞清楚,導緻可見性和有序性問題發生的本質原因是什麼?現在的服務絕大部分都是運作在多核 CPU 的伺服器上,每顆 CPU 都有自己的緩存,這時 CPU 緩存與記憶體的資料就會有一緻性問題了,當一個線程對共享變量的修改,另外一個線程無法立刻看到。導緻可見性問題的本質原因是緩存。

Java 記憶體模型

有序性是指代碼實際的執行順序和代碼定義的順序一緻,編譯器為了優化性能,雖然會遵守 as-if-serial 語義(不管怎麼重排序,在單線程下的執行結果不能改變),不過有時候編譯器及解釋器的優化也可能引發一些問題。比如:雙重檢查來建立單執行個體對象。下面是使用雙重檢查來實作延遲建立單例對象的代碼:

這裡的 instance = new DoubleCheckedInstance();,看起來 Java 代碼隻有一行,應該是無法就行重排序的,實際上其編譯後的實際指令是如下三步:

配置設定對象的記憶體空間

初始化對象

設定 instance 指向剛剛已經配置設定的記憶體位址

上面的第 2 步和第 3 步如果改變執行順序也不會改變單線程的執行結果,也就是說可能會發生重排序,下圖是一種多線程并發執行的場景:

Java 記憶體模型

此時線程 B 擷取到的 instance 是沒有初始化過的,如果此來通路 instance 的成員變量就可能觸發空指針異常。導緻有序性問題的本質原因是編譯器優化。那你可能會想既然緩存和編譯器優化是導緻可見性問題和有序性問題的原因,那直接禁用掉不就可以徹底解決這些問題了嗎,但是如果這麼做了的話,程式的性能可能就會受到比較大的影響了。

其實可以換一種思路,能不能把這些禁用緩存和編譯器優化的權利交給編碼的工程師來處理,他們肯定最清楚什麼時候需要禁用,這樣就隻需要提供按需禁用緩存和編譯優化的方法即可,使用比較靈活。是以Java 記憶體模型就誕生了,它規範了 JVM 如何提供按需禁用緩存和編譯優化的方法,規定了 JVM 必須遵守一組最小的保證,這個最小保證規定了線程對共享變量的寫入操作何時對其它線程可見。

順序一緻性模型是一個理想化後的理論參考模型,處理器和程式設計語言的記憶體模型的設計都是參考的順序一緻性模型理論。其有如下兩大特性:

一個線程中的所有操作必須按照程式的順序來執行

所有的線程都隻能看到一個單一的執行操作順序,不管程式是否同步

在工程師視角下的順序一緻性模型如下:

Java 記憶體模型

順序一緻性模型有一個單一的全局記憶體,這個全局記憶體可以通過左右搖擺的開關可以連接配接到任意一個線程,每個線程都必須按照程式的順序來執行記憶體的讀和寫操作。該理想模型下,任務時刻都隻能有一個線程可以連接配接到記憶體,當多個線程并發執行時,就可以通過開關就可以把多個線程的讀和寫操作串行化。

順序一緻性模型中,所有操操作完全按照順序串行執行,但是在 JMM 中就沒有這個保證了,未同步的程式在 JMM 中不僅程式的執行順序是無序的,而且由于本地記憶體的存在,所有線程看到的操作順序也可能會不一緻,比如一個線程把寫共享變量儲存在本地記憶體中,在還沒有重新整理到主記憶體前,其它線程是不可見的,隻有更新到主記憶體後,其它線程才有可能看到。

JMM 對在正确同步的程式做了順序一緻性的保證,也就是程式的執行結果和該程式在順序一緻性記憶體模型中的執行結果相同。

Happens-Before 規則是 JMM 中的核心概念,Happens-Before 概念最開始在 這篇論文 提出,其在論文中使用 Happens-Before 來定義分布式系統之間的偏序關系。在 JSR-133 中使用 Happens-Before 來指定兩個操作之間的執行順序。

JMM 正是通過這個規則來保證跨線程的記憶體可見性,Happens-Before 的含義是前面一個對共享變量的操作結果對該變量的後續操作是可見的,限制了編譯器的優化行為,雖然允許編譯器優化,但是優化後的代碼必須要滿足 Happens-Before 規則,這個規則給工程師做了這個保證:同步的多線程程式是按照 Happens-Before 指定的順序來執行的。目的就是為了在不改變程式(單線程或者正确同步的多線程程式)執行結果的前提下,盡最大可能的提高程式執行的效率。

Java 記憶體模型

JSR-133 規範中定了如下 6 項 Happens-Before 規則:

程式順序規則:一個線程中的每個操作,Happens-Before 該線程中的任意後續操作

螢幕鎖規則:對一個鎖的解鎖操作,Happens-Before 于後面對這個鎖的加鎖操作

volatile 規則對一個 volatile 類型的變量的寫操作,Happens-Before 與任意後面對這個 volatile 變量的讀操作

傳遞性規則:如果操作 A Happens-Before 于操作 B,并且操作 B Happens-Before 于操作 C,則操作 A Happens-Before 于操作 C

start() 規則:如果一個線程 A 執行操作 threadB.start() 啟動線程 B,那麼線程 A 的 start() 操作 Happens-Before 于線程 B 的任意操作

join() 規則:如果線程 A 執行操作 threadB.join() 并成功傳回,那麼線程 B 中的任意操作 Happens-Before 于線程 A 從 threadB.join() 操作成功傳回

JMM 的一個基本原則是:隻要不改變單線程和正确同步的多線程的執行結果,編譯器和處理器随便怎麼優化都可以,實際上對于應用開發人員對于兩個操作是否真的被重排序并不關心,真正關心的是執行結果不能被修改。是以 Happens-Before 本質上和 sa-if-serial 的語義是一緻的,隻是 sa-if-serial 隻是保證在單線程下的執行結果不被改變。

本文主要介紹了記憶體模型的相關基礎知識和相關概念,JMM 屏蔽了不同處理器記憶體模型之間的差異,在不同的處理器平台上給應用開發人員抽象出了統一的 Java 記憶體模型(JMM)。常見的處理器記憶體模型比 JMM 的要弱,是以 JVM 會在生成位元組碼指令時在适當的位置插入記憶體屏障(記憶體屏障的類型會因處理器平台而有所不同)來限制部分重排序。

Java 搬運工 & 終身學習者 @ 微信公衆号「mghio」