在併發編程須要處理的兩個關鍵問題是:線程之間如何通訊 和 線程之間如何同步。java
通訊 是指線程之間以何種機制來交換信息。在命令式編程中,線程之間的通訊機制有兩種:共享內存 和 消息傳遞。程序員
在共享內存的併發模型裏,線程之間共享程序的公共狀態,線程之間經過寫-讀內存中的公共狀態來隱式進行通訊。編程
在消息傳遞的併發模型裏,線程之間沒有公共狀態,線程之間必須經過明確的發送消息來顯式進行通訊。數組
同步 是指程序用於控制不一樣線程之間操做發生相對順序的機制。緩存
在共享內存的併發模型裏,同步是顯式進行的。程序員必須顯式指定某個方法或某段代碼須要在線程之間互斥執行。安全
在消息傳遞的併發模型裏,因爲消息的發送必須在消息的接收以前,所以同步是隱式進行的。數據結構
注意:Java 的併發採用的是共享內存模型,Java 線程之間的通訊老是隱式進行,整個通訊過程對程序員徹底透明;多線程
同步老是顯示進行,必須指定某個方法或者代碼段在線程間互斥執行;併發
在 Java 中,全部實例域、靜態域 和 數組元素存儲在堆內存中,堆內存在線程之間共享。局部變量、方法定義參數 和 異常處理器參數 不會在線程之間共享,它們不會有內存可見性問題,也不受內存模型的影響。app
Java 線程之間的通訊由 Java 內存模型(JMM)控制。JMM 決定了一個線程對共享變量的寫入什麼時候對另外一個線程可見。從抽象的角度來看,JMM 定義了線程與主內存之間的抽象關係:線程之間的共享變量存儲在主內存中,每個線程都有一個本身私有的本地內存,本地內存中存儲了該變量以讀/寫共享變量的副本。本地內存是 JMM 的一個抽象概念,並不真實存在。
舉個例子:
本地內存 A 和 B 有主內存共享變量 X 的副本。假設一開始時,這三個內存中 X 的值都是 0。線程 A 正執行時,把更新後的 X 值(假設爲 1)臨時存放在本身的本地內存 A 中。當線程 A 和 B 須要通訊時,線程 A 首先會把本身本地內存 A 中修改後的 X 值刷新到主內存去,此時主內存中的 X 值變爲了 1。隨後,線程 B 到主內存中讀取線程 A 更新後的共享變量 X 的值,此時線程 B 的本地內存的 X 值也變成了 1。
總體來看,這兩個步驟實質上是線程 A 再向線程 B 發送消息,而這個通訊過程必須通過主內存。JMM 經過控制主內存與每一個線程的本地內存之間的交互,來爲 Java 程序員提供內存可見性保證。
在執行程序時爲了提升性能,編譯器和處理器經常會對指令作重排序。重排序分三類:
一、編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,能夠從新安排語句的執行順序。
二、指令級並行的重排序。現代處理器採用了指令級並行技術來將多條指令重疊執行。若是不存在數據依賴性,處理器能夠改變語句對應機器指令的執行順序。
三、內存系統的重排序。因爲處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操做看上去多是在亂序執行。
從 Java 源代碼到最終實際執行的指令序列,會分別經歷下面三種重排序:
上面的這些重排序均可能致使多線程程序出現內存可見性問題。對於編譯器,JMM 的編譯器重排序規則會禁止特定類型的編譯器重排序(不是全部的編譯器重排序都要禁止)。對於處理器重排序,JMM 的處理器重排序規則會要求 Java 編譯器在生成指令序列時,插入特定類型的內存屏障指令,經過內存屏障指令來禁止特定類型的處理器重排序(不是全部的處理器重排序都要禁止)。
JMM 屬於語言級的內存模型,它確保在不一樣的編譯器和不一樣的處理器平臺之上,經過禁止特定類型的編譯器重排序和處理器重排序,爲程序員提供一致的內存可見性保證。
現代的處理器使用寫緩衝區來臨時保存向內存寫入的數據。寫緩衝區能夠保證指令流水線持續運行,它能夠避免因爲處理器停頓下來等待向內存寫入數據而產生的延遲。同時,經過以批處理的方式刷新寫緩衝區,以及合併寫緩衝區中對同一內存地址的屢次寫,能夠減小對內存總線的佔用。雖然寫緩衝區有這麼多好處,但每一個處理器上的寫緩衝區,僅僅對它所在的處理器可見。這個特性會對內存操做的執行順序產生重要的影響:處理器對內存的讀/寫操做的執行順序,不必定與內存實際發生的讀/寫操做順序一致!
舉個例子:
假設處理器A和處理器B按程序的順序並行執行內存訪問,最終卻可能獲得 x = y = 0。具體的緣由以下圖所示:
處理器 A 和 B 同時把共享變量寫入在寫緩衝區中(A一、B1),而後再從內存中讀取另外一個共享變量(A二、B2),最後才把本身寫緩衝區中保存的髒數據刷新到內存中(A三、B3)。當以這種時序執行時,程序就能夠獲得 x = y = 0 的結果。
從內存操做實際發生的順序來看,直處處理器 A 執行 A3 來刷新本身的寫緩存區,寫操做 A1 纔算真正執行了。雖然處理器 A 執行內存操做的順序爲:A1 -> A2,但內存操做實際發生的順序倒是:A2 -> A1。此時,處理器 A 的內存操做順序被重排序了。
這裏的關鍵是,因爲寫緩衝區僅對本身的處理器可見,它會致使處理器執行內存操做的順序可能會與內存實際的操做執行順序不一致。因爲現代的處理器都會使用寫緩衝區,所以現代的處理器都會容許對寫-讀操做重排序。
爲了保證內存可見性,Java 編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序。JMM 把內存屏障指令分爲下列四類:
| 屏障類型 | 指令示例 | 說明 |
| ------------------- | :------------------------: | :--------------------------------------: |
| LoadLoad Barriers | Load1; LoadLoad; Load2 | 確保 Load1 數據的裝載,以前於 Load2 及全部後續裝載指令的裝載。 |
| StoreStore Barriers | Store1; StoreStore; Store2 | 確保 Store1 數據對其餘處理器可見(刷新到內存),以前於 Store2 及全部後續存儲指令的存儲。 |
| LoadStore Barriers | Load1; LoadStore; Store2 | 確保 Load1 數據裝載,以前於 Store2 及全部後續的存儲指令刷新到內存。 |
| StoreLoad Barriers | Store1; StoreLoad; Load2 | 確保 Store1 數據對其餘處理器變得可見(指刷新到內存),以前於 Load2 及全部後續裝載指令的裝載。StoreLoadBarriers 會使該屏障以前的全部內存訪問指令(存儲和裝載指令)完成以後,才執行該屏障以後的內存訪問指令。 |
JSR-133 內存模型使用 happens-before 的概念來闡述操做之間的內存可見性。在 JMM 中,若是一個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必需要存在 happens-before 關係。這裏提到的兩個操做既能夠是在一個線程以內,也能夠是在不一樣線程之間。
與程序員密切相關的 happens-before 規則以下:
注意,兩個操做之間具備 happens-before 關係,並不意味着前一個操做必需要在後一個操做以前執行!happens-before 僅僅要求前一個操做(執行的結果)對後一個操做可見,且前一個操做按順序排在第二個操做以前(the first is visible to and ordered before the second)。
若是咱們仔細分析 concurrent 包的源代碼實現,會發現一個通用化的實現模式:
AQS,非阻塞數據結構和原子變量類(java.util.concurrent.atomic 包中的類),這些 concurrent 包中的基礎類都是使用這種模式來實現的,而 concurrent 包中的高層類又是依賴於這些基礎類來實現的。從總體來看,concurrent 包的實現示意圖以下:
如上圖所示,一個 happens-before 規則對應於一個或多個編譯器和處理器重排序規則。
as-if-serial 語義的意思指:無論怎麼重排序(編譯器和處理器爲了提升並行度),(單線程)程序的執行結果不能被改變。編譯器,runtime 和處理器都必須遵照 as-if-serial 語義。
爲了遵照 as-if-serial 編譯器和處理器不會對存在數據依賴關係的操做作重排序,由於這種重排序會改變執行結果。可是若是操做之間沒有數據依賴關係,這些操做就可能被編譯器和處理器重排序。
Java 程序的內存可見性保證按程序類型能夠分爲下列三類:
1.單線程程序。單線程程序不會出現內存可見性問題。編譯器,runtime 和處理器會共同確保單線程程序的執行結果與該程序在順序一致性模型中的執行結果相同。
2.正確同步的多線程程序。正確同步的多線程程序的執行將具備順序一致性(程序的執行結果與該程序在順序一致性內存模型中的執行結果相同)。這是 JMM 關注的重點,JMM經過限制編譯器和處理器的重排序來爲程序員提供內存可見性保證。
3.未同步/未正確同步的多線程程序。JMM 爲它們提供了最小安全性保障:線程執行時讀取到的值,要麼是以前某個線程寫入的值,要麼是默認值(0,null,false)。
下圖展現了這三類程序在 JMM 中與在順序一致性內存模型中的執行結果的異同: