java內存模型(JMM)與併發

爲什麼要併發

充分利用CPU計算能力

隨時多核處理器的發展,計算機的計算能力越來越強大。但是,由於磁盤讀寫、網絡IO等這些通信、存儲系統與CPU的速度相差多個數量級,我們在應用計算機時,如果單純的讓cpu等待磁盤IO、網絡IO便是對cpu的計算能力的浪費。

那麼如何讓CPU的能力充分利用呢,我們自然會想到讓多個任務同時執行,不必因爲一個任務等待在IO上而讓這個cpu也同時等待(佔用),這時可以讓這個cpu去處理別的任務

提升服務端的服務能力

我們除了充分利用CPU,在現實中,人們更希望一個服務能夠良好的爲多個客戶端提供服務。作爲服務端的工程師們,自然也會想到如何更好地併發處理多個任務了。通常衡量一個服務端併發能力的指標是TPS(transaction per second),

提升響應速度,提供更好地體驗

當你在taobao上購買一個商品,你肯定願意更快速的完成支付。在支付成功,會有生成訂單、庫存記錄、郵件通知等等步驟,使用併發能夠同時執行多個任務,事半功倍。

硬件層面優化

高速緩存

剛纔說了,cpu的速度與存儲等系統的速度差距巨大,但是使用cpu不可能避免去主內存讀取數據,爲了提高整體的計算能力,現代計算機在主存與CPU之間加入了高速緩存提高cpu整體的運算能力。當cpu做計算時,先將需要的數據存儲在高速緩存上,cpu直接與高速緩存做交互,當cpu計算完時再將高速緩存的數據同步到主內存中,這樣避免了多次與較慢的主內存通信,拖慢速度。

但是在多處理器計算機中,對每個cpu上加一層高速緩存的做法,也會引入更多的複雜性。如果多個cpu的高速緩存上存儲了同一個變量,計算後,每個高速緩存的變量值不同,那麼到底以誰的數據爲準呢?

緩存一致性(Cache Coherence)
爲了保證多個cpu的緩存與內存的同步有序可控,也就是爲了解決緩存一致性問題,需要各個處理器訪問緩存時遵守一些協議:如MESI等等。通過這些協議來保證緩存的一致性。

重排序優化

cpu還會對輸入的指令進行亂序重排,以提高執行速度。這個亂序重排是針對每個計算單元內的指令(計算單元就是非原子性的執行指令),對於每個計算單元執行後的結果,會對着這些結果進行重組保證整體執行結果正確性。但是每個計算單元內部的順序無法保證按序執行,如果一個計算單元依賴於另一個計算單元的中間狀態,那麼這樣的亂序執行結果就無法保證正確。
除了cpu的重排序,JIT編譯器也有類似的重排序優化。

JMM(java memory model)

JMM-java內存模型,就是通過定義一套抽象規則,去屏蔽底層操作系統、硬件對內存訪問的差異,使得JAVA代碼執行時對主存的訪問效果是一致的。

這裏寫圖片描述

JMM中定義的主存,類比於操作系統中的主存,主要存放線程間的共享數據(除去方法參數、局部變量)。
JMM中的每個線程都有一個WorkingMemory,是主內存中部分數據的副本,只能該線程訪問,類比於硬件的高速緩存。線程對數據的訪問通常只能在workingMemory中進行,不能直接與主存交互。

WorkingMemory與Main Memory之間的交互

JMM定義了8種操作,來完成WorkingMemory與MainMemory之間的交互,這8中操作都是原子性的(對於64bit的long\double變量來說,某些平臺的這8中操作並不是原子性的)。

  • Lock: 作用於主內存MainMemory,鎖住變量所在的MainMemory部分,不允許其他線程訪問,排他訪問。
  • unlock:與lock對應,解鎖。
  • read:作用於主內存上,把主內存上的某個數據傳輸到workingMemory上,以便後續的load指令使用。
  • load:作用於workingMemory上,把從主內存傳輸的數據移動到workingMemory對應的變量副本上。
  • use :作用於workingMemroy,它把workingMemroy中的變量副本的值傳遞給java執行引擎。jvm每當執行到需要使用某個變量時都會執行這個操作。
  • assign:作用於workingMemory,把執行引擎中的某個值賦給workingMemory中的某個變量副本。JMV每當遇到一個賦值的字節碼指令時,都會執行這個操作。
  • store:作用於workingMemory,把workingMemory中的某個變量值傳輸到主內存中,以便後續執行write操作。
  • write:作用於主內存,把store操作傳輸過來的數據放入主內存中寫入。

這裏寫圖片描述

對於上述命令的執行,必須遵守的規則
1.load、read 與 store、write 必須遵守先後順序,但是它們之間可插入其他命令,也就是一個load(store)後邊必須對應一個read(write)。如果要把一個變量從workingMemory中同步到主內存中,需要順序的執行store、write。同樣,要把一個變量從主內存同步到workingMemory中要順序執行read、load。
2.不允許workingMemory中的數據在沒有執行assign指令時,將數據同步到主內存中。
3.一個新的變量必須要首先要在主存中出現,也就是說先要執行了assign、load之後才允許執行對這個變量的use、store指令。
4.同一時刻只允許一個線程對變量執行lock操作,一個lock操作必須對應一個unlock操作
5.對一個變量執行unlock之前,需要把這個變量同步回主存(執行 store、write操作)
6.在執行lock時,會清空這個cpu對應的緩存數據,在執行引擎使用(use)這個數據之前,需要重新load或assign這個workingMemory中的緩存值。
7.在執行unlock時,需要把這個變量同步回主存(store,write)。

原子性、可見性、有序性

前文說了,JMM對不同操作系統、硬件的屏蔽後,抽象出來的一套線程本地內存對主存訪問的統一描述規則。具體主要圍繞着可見性、原子性、有序性展開描述。

原子性:
JMM定義了 lock、read、load、use、assign、store、write、unlock原子性指令(long\doubel 64bit數據操作允許非原子性,但目前很少有非原子性實現的JVM),來保證指令的原子性。在更大範圍的計算中如何保證原子性?java中通過使用lock、unlock指令可以保證更大範圍的原子性操作,在字節碼層面對應monitorenter、monitorexit指令;在JDK中對應的就是synchronized關鍵字。

可見性:
synchronized:可見性通過 lock\unlock直接讀取主內存刷新緩存來完成;
volatile:可見性通過對volatile修飾的變量,直接立即從主存讀取實現;
final:通過插入屏障禁止重排序,保證每次讀取final變量時獲取已經初始化完成的值,並且final變量的引用無法更改。

有序性:
synchronized:通過使用lock\unlock 指令,限定一個線程獨佔訪問,來實現有序性。
volatile:通過使用內存屏障禁止重排序。
final:同樣是使用 內存屏障禁止重排序,保證一個線程觀察另一個線程中的final變量總是初始化完成的。

1.Within-Thread As-If-Serial Semantics.
2.Without-Thread,reodrdering:指令重排序、工作內存與主內存同步延遲

volatile關鍵字本身就包含了禁止指令重排序的語義,而synchronized則是由“一個變量在同一個時刻
只允許一條線程對其進行lock操作

happens-before

happens-before這個原則非常重要,它是判斷數據是否存在競爭、 線程是否安全的主要依據,依靠這個原則, 如果說操作A先行發生於操作B,其實就是說在發生操作B之前,操作A產 生的影響能被操作B觀察到,“影響”包括修改了內存中共享變量的值、 發送了消息、 調用了 方法等

下面是Java內存模型下一些“天然的”先行發生關係,
這些先行發生關係無須任何同步器協助就已經存在,可以在編碼中直接使用。
如果兩個操作之間的關係不在此列,並且無法從下列規則推導出來的話,它們就沒有順序性保障,虛擬機可以對它們隨意地進行重排序。

  • Program Order Rule:在一個線程內,按照程序代碼順序,書寫在前面 的操作先行發生於書寫在後面的操作(代碼不存在依賴關係是可以被重排序的,但是從我們感知來說依舊是順序執行的,as-if-serial)

  • Monitor Lock Rule:一個unlock操作先行發生於後面對同一個鎖的lock操作。 這裏必須強調的是同一個鎖,而“後面”是指時間上的先後順序.

  • Volatile Variable Rule:對一個volatile變量的寫操作先行發生於後面對這個變量的讀操作,這裏的“後面”同樣是指時間上的先後順序。

  • Thread Start Rule:Thread對象的start()方法先行發生於此線程的每一個動作。

  • Thread Termination Rule:線程中的所有操作都先行發生於對此線程的終止檢測

  • Thread Interruption Rule:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生

  • 對象終結規則(Finalizer Rule):一個對象的初始化完成(構造函數執行結束)先行發生於它的finalize()方法的開始。

  • Transitivity:如果操作A先行發生於操作B,操作B先行發生於操作C,那就
    可以得出操作A先行發生於操作C的結論。

    一個操作“時間上的先發生”不代表這個操作會是“先行發生”,一個操作“先行發生”也不能推導出這個操作必定是“時間上的先發生”。
    也就是說happenbefore中的先行發生與我們常識中的時間先後的發生沒有關係。這裏的先行發生主要是對後續發生的操作產生影響!

線程的實現

線程是比進程更輕量級的調度執行單位,線程的引入,可以把一個進程的資
源分配和執行調度分開,各個線程既可以共享進程資源(內存地址、 文件I/O等),又可以
獨立調度(線程是CPU調度的基本單位)。

主流的操作系統都提供了線程實現,Java語言則提供了在不同硬件和操作系統平臺下對
線程操作的統一處理,每個已經執行start()且還未結束的java.lang.Thread類的實例就代表
了一個線程。 我們注意到Thread類與大部分的Java API有顯著的差別,它的所有關鍵方法都
是聲明爲Native的。 在Java API中,一個Native方法往往意味着這個方法沒有使用或無法使用
平臺無關的手段來實現(當然也可能是爲了執行效率而使用Native方法,不過,通常最高效
率的手段也就是平臺相關的手段)。 正因爲如此,作者把本節的標題定爲“線程的實現”而不
是“Java線程的實現”。
實現線程主要有3種方式:使用內核線程實現、 使用用戶線程實現和使用用戶線程加輕
量級進程混合實現。

http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#whatismm
https://www.cs.umd.edu/users/pugh/java/memoryModel/

發佈了126 篇原創文章 · 獲贊 196 · 訪問量 26萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章