1. 概述
多任務和高并發是衡量一台計算機處理器的能力重要名額之一。一般衡量一個伺服器性能的高低好壞,使用每秒事務處理數(Transactions Per Second,TPS)這個名額比較能說明問題,它代表着一秒内伺服器平均能響應的請求數,而TPS值與程式的并發能力有着非常密切的關系。在讨論Java記憶體模型和線程之前,先簡單介紹一下硬體的效率與一緻性。
2.硬體的效率與一緻性
由于計算機的儲存設備與處理器的運算能力之間有幾個數量級的差距,是以現代計算機系統都不得不加入一層讀寫速度盡可能接近處理器運算速度的高速緩存(cache)來作為記憶體與處理器之間的緩沖:将運算需要使用到的資料複制到緩存中,讓運算能快速進行,當運算結束後再從緩存同步回記憶體之中沒這樣處理器就無需等待緩慢的記憶體讀寫了。
基于高速緩存的存儲互動很好地解決了處理器與記憶體的速度沖突,但是引入了一個新的問題:緩存一緻性(Cache Coherence)。在多處理器系統中,每個處理器都有自己的高速緩存,而他們又共享同一主存,如下圖所示:多個處理器運算任務都涉及同一塊主存,需要一種協定可以保障資料的一緻性,這類協定有MSI、MESI、MOSI及Dragon Protocol等。Java虛拟機記憶體模型中定義的記憶體通路操作與硬體的緩存通路操作是具有可比性的,後續将介紹Java記憶體模型。
image
除此之外,為了使得處理器内部的運算單元能竟可能被充分利用,處理器可能會對輸入代碼進行亂起執行(Out-Of-Order Execution)優化,處理器會在計算之後将對亂序執行的代碼進行結果重組,保證結果準确性。與處理器的亂序執行優化類似,Java虛拟機的即時編譯器中也有類似的指令重排序(Instruction Recorder)優化。
3.Java記憶體模型
定義Java記憶體模型并不是一件容易的事情,這個模型必須定義得足夠嚴謹,才能讓Java的并發操作不會産生歧義;但是,也必須得足夠寬松,使得虛拟機的實作能有足夠的自由空間去利用硬體的各種特性(寄存器、高速緩存等)來擷取更好的執行速度。經過長時間的驗證和修補,在JDK1.5釋出後,Java記憶體模型就已經成熟和完善起來了。
3.1 主記憶體與工作記憶體
Java記憶體模型的主要目标是定義程式中各個變量的通路規則,即在虛拟機中将變量存儲到記憶體和從記憶體中取出變量這樣底層細節。此處的變量與Java程式設計時所說的變量不一樣,指包括了執行個體字段、靜态字段和構成數組對象的元素,但是不包括局部變量與方法參數,後者是線程私有的,不會被共享。
Java記憶體模型中規定了所有的變量都存儲在主記憶體中,每條線程還有自己的工作記憶體(可以與前面将的處理器的高速緩存類比),線程的工作記憶體中儲存了該線程使用到的變量到主記憶體副本拷貝,線程對變量的所有操作(讀取、指派)都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變量。不同線程之間無法直接通路對方工作記憶體中的變量,線程間變量值的傳遞均需要在主記憶體來完成,線程、主記憶體和工作記憶體的互動關系如下圖所示,和上圖很類似。
這裡的主記憶體、工作記憶體與Java記憶體區域的Java堆、棧、方法區不是同一層次記憶體劃分。
3.2 記憶體間互動操作
關于主記憶體與工作記憶體之間的具體互動協定,即一個變量如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步到主記憶體之間的實作細節,Java記憶體模型定義了以下八種操作來完成:
- lock(鎖定):作用于主記憶體的變量,把一個變量辨別為一條線程獨占狀态。
- unlock(解鎖):作用于主記憶體變量,把一個處于鎖定狀态的變量釋放出來,釋放後的變量才可以被其他線程鎖定。
- read(讀取):作用于主記憶體變量,把一個變量值從主記憶體傳輸到線程的工作記憶體中,以便随後的load動作使用
- load(載入):作用于工作記憶體的變量,它把read操作從主記憶體中得到的變量值放入工作記憶體的變量副本中。
- use(使用):作用于工作記憶體的變量,把工作記憶體中的一個變量值傳遞給執行引擎,每當虛拟機遇到一個需要使用變量的值的位元組碼指令時将會執行這個操作。
- assign(指派):作用于工作記憶體的變量,它把一個從執行引擎接收到的值指派給工作記憶體的變量,每當虛拟機遇到一個給變量指派的位元組碼指令時執行這個操作。
- store(存儲):作用于工作記憶體的變量,把工作記憶體中的一個變量的值傳送到主記憶體中,以便随後的write的操作。
- write(寫入):作用于主記憶體的變量,它把store操作從工作記憶體中一個變量的值傳送到主記憶體的變量中。
如果要把一個變量從主記憶體中複制到工作記憶體,就需要按順尋地執行read和load操作,如果把變量從工作記憶體中同步回主記憶體中,就要按順序地執行store和write操作。Java記憶體模型隻要求上述操作必須按順序執行,而沒有保證必須是連續執行。也就是read和load之間,store和write之間是可以插入其他指令的,如對主記憶體中的變量a、b進行通路時,可能的順序是read a,read b,load b, load a。Java記憶體模型還規定了在執行上述八種基本操作時,必須滿足如下規則:
- 不允許read和load、store和write操作之一單獨出現
- 不允許一個線程丢棄它的最近assign的操作,即變量在工作記憶體中改變了之後必須同步到主記憶體中。
- 不允許一個線程無原因地(沒有發生過任何assign操作)把資料從工作記憶體同步回主記憶體中。
- 一個新的變量隻能在主記憶體中誕生,不允許在工作記憶體中直接使用一個未被初始化(load或assign)的變量。即就是對一個變量實施use和store操作之前,必須先執行過了assign和load操作。
- 一個變量在同一時刻隻允許一條線程對其進行lock操作,lock和unlock必須成對出現
- 如果對一個變量執行lock操作,将會清空工作記憶體中此變量的值,在執行引擎使用這個變量前需要重新執行load或assign操作初始化變量的值
- 如果一個變量事先沒有被lock操作鎖定,則不允許對它執行unlock操作;也不允許去unlock一個被其他線程鎖定的變量。
- 對一個變量執行unlock操作之前,必須先把此變量同步到主記憶體中(執行store和write操作)。
3.3 重排序
在執行程式時為了提高性能,編譯器和處理器經常會對指令進行重排序。重排序分成三種類型:
- 編譯器優化的重排序。編譯器在不改變單線程程式語義放入前提下,可以重新安排語句的執行順序。
- 指令級并行的重排序。現代處理器采用了指令級并行技術來将多條指令重疊執行。如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序。
- 記憶體系統的重排序。由于處理器使用緩存和讀寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。
從Java源代碼到最終實際執行的指令序列,會經過下面三種重排序:
為了保證記憶體的可見性,Java編譯器在生成指令序列的适當位置會插入記憶體屏障指令來禁止特定類型的處理器重排序。Java記憶體模型把記憶體屏障分為LoadLoad、LoadStore、StoreLoad和StoreStore四種:
3.4 同步機制
介紹volatile、synchronized和final
3.5 原子性、可見性與有序性
介紹三個特性
歡迎工作一到五年的Java程式員朋友們加入Java架構交流:810589193
本群提供免費的學習指導 架構資料 以及免費的解答
不懂得問題都可以在本群提出來 之後還會有職業生涯規劃以及面試指導