深入理解Java虚拟机(周志明第三版)- 第十二章:Java内存模型与线程
系列文章目錄
第一章: 走近Java
第二章: Java內存區域與內存溢出異常
第三章: Java垃圾收集器與內存分配策略
并發處理的廣泛應用是Amdahl定律代替摩爾定律成為計算機性能發展源動力的根本原因,也是人類壓榨計算機運算能力的最有力武器
- 系列文章目錄
- 一、概述
- 二、硬件的效率和一致性
- 三、Java內存模型
- 1、主內存與工作內存
- 2、內存間交互
- 3、對于volatile型變量的特殊規則
- 4、針對long和double變量的特殊規則
- 5、原子性、可見性與有序性
- 原子性
- 可見性
- 有序性
- 6、先行發生規則
- 四、Java與線程
- 1、線程的實現
- 使用內核線程實現(1:1實現)
- 使用用戶線程實現(1:N實現)
- 使用用戶線程加輕量級進程混合實現(N:M實現)
- Java線程如何實現
- 2、Java線程調度
- 3、狀態轉換
- 五、Java與協程(了解即可)
- 1、內核線程的局限
- 2、協程的復蘇
- 3、Java的解決方案
- 六、附錄
一、概述
????????多任務處理是現代計算機幾乎必備的功能,不僅是因為計算機的運算能力強大了,還有一個很重要的原因是計算機的運算速度與它的存儲和通信子系統的速度差距太大,大量的時間花費在磁盤IO、網絡通信或數據庫訪問上,為了不希望處理器在大部分時間里都處于等待其他資源的空閑狀態,就必須使用一些手段壓榨處理器的運算能力,否則會造成很大的性能浪費,而讓計算機同時處理幾項任務則是最容易想到的,也被證明是非常有效的壓榨手段。
????????另外一種更具體的并發應用場景就是一個服務端同時對多個客戶端提供服務。衡量一個服務性能的好壞,每秒事務處理數(TPS)是重要的指標之一,它代表著一秒內服務端平均能響應的請求總數,而TPS與程序的并發能力有非常密切的關系。對于計算量相同的任務,線程并發協調越有條不紊,效率越高;線程間競爭頻繁,互相阻塞甚至死鎖,將會大大降低程序并發能力。
????????服務端應用是Java語言擅長的領域之一。而Java語言和虛擬機提供了許多工具,將并發編程的門檻降低了不少,且各種中間件、各類框架等也可能隱藏線程并發細節,使得程序員在編碼時更關注業務邏輯。
但是無論語言、中間件和框架再如何先進,開發人員都不應期望它們能獨立完成所有并發處理的事情,了解并發的內幕仍然是成為一個高級程序員不可缺少的課程。
二、硬件的效率和一致性
什么是高速緩存?為什么需要高速緩存
????????物理機絕大多數的計算任務都不可能只靠處理器計算完成,處理器至少要與內存交互,如讀取運算數據、存儲運算結果等,無法僅僅依靠寄存器完成所運算任務。由于計算機的存儲設備與處理器的運算速度有幾個數量級的差距,所以現代計算機不得不加入一層或多層讀寫速度盡可能接近處理器運算速度的高速緩存來作為內存與處理器之間的緩沖:將運算需要使用的數據復制到緩存中,讓運算能快速進行,當運算結束后再從緩存同步回內存中,這樣處理器就無須等待緩慢的內存讀寫了。
什么是緩存一致性問題?如何解決?
在多路處理器系統中,每個處理器都有自己的高速緩存,而它們又共享同一主內存,這種系統成為共享多核系統,當多個處理器的運算任務都涉及同一塊主內存區域時,將可能導致各自的緩存數據不一致,如果發生這種情況,那同步回到主內存時該以誰的緩存數據為準呢?為了解決一致性問題,需要各個處理器訪問緩存時遵循一些協議,在讀寫時要根據協議來進行操作,這類協議有MSI、MESI等。
內存模型可以理解為在特定的操作協議下,對特定的內存或高速緩存進行讀寫訪問的過程抽象。不同架構的物理機器可以擁有不一樣的內存模型。
除了增加高速緩存之外,為了使處理器內部的運算單元能盡量被充分使用,處理器可能會對輸入代碼進行亂序執行優化,處理器會在計算之后將亂序執行的結果重組,保證該結果與順序執行的結果是一致的,但并不保證程序中各個語句計算的先后順序與輸入代碼中的順序一致,因此如果存在一個計算任務依賴另外一個計算任務的中間結果,那么其順序性并不能靠代碼的先后順序來保證。與處理器的亂序執行優化類似,Java虛擬機的即時編譯器也有指令重排序優化。
三、Java內存模型
Java內存模型屏蔽各種硬件和操作系統的內存訪問差異,以實現讓Java程序在各種平臺下都能達到一致的內存訪問效果。
1、主內存與工作內存
????????Java內存模型的主要目的是定義了程序中各種變量的訪問規則,即關注在虛擬機中把變量值存儲到內存和從內存中取出變量值這樣的底層操作細節。這里的變量包括了實例字段、靜態字段和構成數組對象的元素,但是不包括局部變量與方法參數,因為后者是線程私有的,不會被共享,自然就不存在競爭問題,且為了獲得更好的執行效能,Java內存模型并沒有限制執行引擎使用處理器的特定寄存器或緩存來和主內存進行交互,也沒有限制即時編譯器是否需要進行調整代碼執行順序這類優化措施。
Java內存模型規定了所有的變量都存儲在主內存中,每個線程還有自己的工作內存(類比高速緩存),線程的工作內存中保存了該線程使用的變量的主內存副本,線程對變量的所有讀寫操作都必須在工作內存中進行,而不能直接讀寫主內存中的數據,不同線程之間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞都需要通過主內存來完成。如圖:
2、內存間交互
????????關于主內存與工作內存之間具體的交互協議,即一個變量如何從主內存拷貝到工作內存、如何從工作內存同步回主內存這一類的實現細節。Java內存模型定義了以下8種操作來完成,Java虛擬機實現時必須保證下面提及的每一種操作都是原子的、不可再分的:
- lock(鎖定):作用于主內存的變量,將一個變量標識為一條線程獨占的狀態
- unlock(解鎖):作用于主內存的變量,將一個處于鎖定狀態的變量釋放出來,釋放后的變量才可以被其他線程鎖定
- read(讀取):作用于主內存的變量,將一個變量的值從主內存傳輸到線程的工作內存中,以便隨后的load使用
- load(載入):作用于工作內存的變量,將read操作讀取的變量值放入工作內存的變量副本中
- use(使用):作用于工作內存的變量,將工作內存中變量值傳遞給執行引擎,每當虛擬機遇到需要使用變量值的字節碼指令時將執行該操作
- assign(賦值):作用于工作內存的變量,將從執行引擎接收的值賦給工作內存的變量,每當虛擬機遇到給變量賦值的字節碼指令時執行該操作
- store(存儲):作用于工作內存的變量,將工作內存中一個變量的值傳送到主內存中,以便隨后的write使用
- write(寫入):作用于主內存的變量,將store操作從工作內存取到的變量值放入主內存的變量中
如果將一個變量從主內存拷貝到工作內存中,就要按順序執行read和load操作,如果把變量從工作內存同步回主內存,就要按順序執行store和write操作。Java內存模型只要求上述兩個操作必須順序執行,但不要求是連續執行,即可能:read:a read:b load:b load:a。除此之外,Java內存模型還規定了在執行上述8種基本操作時必須滿足如下規則:
- 不允許read和load、store和write單一出現,即不允許一個變量從主內存讀取了但工作內存不接收,或工作內存發起回寫但主內存不接收的情況
- 不允許一個線程丟失它最近的assign操作,即變量在工作內存中改變后必須把變化同步回主內存
- 不允許一個線程無原因地把數據從線程的工作內存同步回主內存中
- 一個新的變量只能在主內存中“誕生”,不允許在工作內存中直接使用一個未經初始化的變量,即對一個變量use、store操作前,必須先執行assign和load操作
- 一個變量在同一時刻只允許一條線程對其進行lock操作,但lock操作可以被同一條線程重復執行多次,多次執行lock,只有執行相同次數的unlock操作,變量才會解鎖
- 如果對一個變量執行lock操作,那將會清空工作內存中此變量的值,執行引擎使用這個變量值前,需要重新執行load或assign操作以初始化變量的值
- 如果一個變量事先沒有被lock操作鎖定,那就不允許對它執行unlock操作,也不允許unlock一個被其他線程鎖定的變量
- 對一個變量執行unlock操作前,必須先把此變量同步回主內存中,
3、對于volatile型變量的特殊規則
關鍵字volatile是虛擬機提供的最輕量級的同步機制。
變量修飾為volatile有什么用?
1、保證此變量對所有線程的可見性,即當一條線程修改了這個變量的值,新值對于其他線程來說是可以立即得知的,而普通變量的值在線程間傳遞時均需要通過主內存來完成,例線程A修改一個普通變量的值后,然后向主內存進行回寫,另外一條線程B在線程A回寫完成后再對主內存進行讀取操作,新變量值才對線程B可見。
volatile變量在各個線程的工作內存中是不存在一致性問題的(物理存儲角度看,各個線程的工作內存中volatile變量也可以存在不一致的情況,但由于每次使用之前都要先刷新,執行引擎看不到不一致的情況,因此可以認為不存在一致性問題,但是Java里運算操作符并非原子操作(自增++、自減–等),這導致volatile變量的運算在并發下一樣是不安全的)
????????
由于volatile變量只能保證可見性,在不符合以下兩條規則的運算場景下,仍然要通過加鎖(synchronized、current并發包中的鎖或原子類)來保證原子性:
????????-運算結果不依賴變量的當前值,或者確保只有單一的線程修改變量值
????????-變量不需要與其他的狀態變量共同參與不變約束
2、禁止指令重排序優化,普通的變量僅會保證在該方法的執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操作的順序與程序代碼中的執行順序一致
4、針對long和double變量的特殊規則
什么是long和double的非原子性協定?
????????Java內存模型要求lock、unlock、read、load、assign、use、store、write這8種操作都具有原子性,但是對于64位的數據類型,在模型中定義了一條寬松的規定:允許虛擬機將沒有被volatile修飾的64位數據類型的讀寫操作劃分為兩次32位的操作來進行,即允許虛擬機實現自行選擇是否要保證64位數據類型的load、store、read、write這4個操作的原子性。
5、原子性、可見性與有序性
原子性
由Java內存模型直接保證的原子性變量操作包括read、load、assign、use、store和write這6個,大致可以認為,基本數據類型的訪問、讀寫都是原子性的,此外更大范圍的原子性保證,Java內存模型還提供了lock和unlock操作來滿足需求,如更高層次的字節碼指令monitorenter和monitorexit來隱式地使用,這兩個字節碼指令反映到Java代碼中就是同步塊-synchronized關鍵字
可見性
指當一個線程修改了變量值后,其他線程能夠立即得知這個修改。volatile變量的特殊規則保證了新值能立即同步到主內存中,以及每次使用前立即從主內存刷新。
有序性
在本線程內觀察,所有的操作都是有序的(線程內似表現為串行的語義);如果在一個線程中觀察另一個線程,所有的操作都是無序的(指令重排序現象和工作內存與主內存同步延遲現象)。
6、先行發生規則
為什么有這個規則?
如果Java內存模型中所有的有序性都依靠volatile和synchronized來完成,那么將會有很多操作變得非常啰嗦,但是我們在編寫Java程序時并沒有察覺到這一點,就是因為Java語言中有一個先行發生原則
什么是先行發生?
先行發生是Java內存模型中定義的兩項操作之間的偏序關系。例如操作A先行發生于操作B,其實就是說在發生操作B之前,操作A產生的影響能夠被操作B觀察到,"影響"包括了修改內存中共享變量的值、發送消息、調用了方法等。
什么是Java內存模型的先行發生規則?
- 程序次序規則:在一個線程內,按照控制流順序,書寫在前面的操作先行發生于書寫在后面的操作
- 管程鎖定規則:一個unlock操作先行發生于后面對同一個鎖的lock操作
- volatile變量規則:對一個volatile變量的寫操作先行發生于后面對這個變量的讀操作
- 線程啟動規則:Thread對象的start()方法先行發生于此線程的每個動作
- 線程終止規則:線程中所有操作都先行發生于對此線程的終止檢測
- 線程中斷規則:對線程interrupt()方法的調用先行發生于被中斷的代碼檢測到中斷事件的發生
- 對象終結規則:一個對象的初始化操作先行發生于它的finalize()方法的開始
- 傳遞性:如果操作A先行發生于操作B,操作B先行發生于操作C,那么可以得出操作A先行發生于操作C
Java內存模型的先行發生規則有什么用?
滿足上述先行發生規則的場景無需使用任何同步手段保障。
四、Java與線程
1、線程的實現
線程是比進程更輕量級的調度執行單元,線程的引入可以把一個進程的資源分配和執行調度分開,各個線程既可以共享進程資源(內存地址、文件IO等),又可以獨立調度。
實現線程主要有三種方式:
使用內核線程實現(1:1實現)
內核線程指直接由操作系統內核支持的線程,由內核來完成線程切換,內核通過操縱調度器對線程進行調度,并負責將線程的任務映射到各個處理器上。
輕量級進程是內核線程的一種高級接口,也是我們通常意義講的線程,每一個輕量級進程都由一個內核線程支持。
優劣:
每個輕量級進程都成為一個獨立的調度單元,即使其中一個輕量級進程阻塞了,也不會影響整個進程繼續工作。但由于是基于內核線程實現的,各種線程操作需要進行系統調用,代價相對較高,需要在用戶態和內核態來回切換,且一個輕量級進程對應一個內核線程,輕量級進程要消耗一定的內核資源(內核線程的??臻g),因此一個系統支持輕量級進程的數據是有限的
使用用戶線程實現(1:N實現)
用戶線程指完全建立在用戶空間的線程庫上,系統內核不能感知到用戶線程的存在及如何實現的。用戶線程的創建、同步、銷毀、調度完全在用戶態中完成,不需要內核的幫助。如果程序實現得當,這種線程不需要切換到內核態,因此操作可以是非??焖偾业拖牡?#xff0c;也能夠支持更大的線程數量。
優劣:
用戶線程的優勢在于不需要系統內核支援,劣勢也在于沒有系統內核支援,所有的線程操作都需要用戶程序自己去處理,因此用戶線程實現的程序通常都比較復雜。
使用用戶線程加輕量級進程混合實現(N:M實現)
混合實現下,既存在用戶線程,也存在輕量級進程。用戶線程還是完全建立在用戶空間中,因此用戶線程的各種操作依然廉價,且可以支持大規模的用戶并發。而操作系統支持的輕量級進程則作為用戶線程和內核線程之間的橋梁,這樣做可以使用內核提供的線程調度功能及處理器映射,并且用戶線程的系統調用要通過輕量級進程來完成,大大降低了整個進程被完全阻塞的風險。
Java線程如何實現
Java線程的實現并不受Java虛擬機規范的約束,這是一個與具體虛擬機相關的話題。
目前主流平臺上的主流商用虛擬機的線程模型普遍使用基于操作系統原生線程模型實現,即1:1實現模型
Hotspot虛擬機中,它的每一個Java線程都是直接映射到一個操作系統原生線程來實現的,而且中間沒有額外的間接結構,所以Hotspot是不會干涉線程調度的,全權交給底層的操作系統去處理。
2、Java線程調度
線程調度指系統為線程分配處理器使用權的過程,調度主要方式有兩種:協同式線程調度和搶占式線程調度。
協同式線程調度:
指線程的執行時間由線程本身控制,線程把自己的工作執行完后,要主動通知系統切換到另外一個線程上去。協同式調度的好處是實現簡單且切換動作對自己本身是可知的,一般沒有什么線程同步的問題;壞處是線程執行時間不可控,可能會一個線程阻塞影響整個進程或系統
搶占式線程調度:
指每個線程的執行時間由系統來分配,線程的切換不由線程本身決定。在這種實現線程調度方式下,線程的執行時間是系統可控的,也不會因為一個線程導致整個進程或系統阻塞
Java使用的線程調度方式是搶占式線程調度。Java語言一共設置了10個級別的線程優先級。當兩個線程同時處于Ready狀態時,優先級越高的線程越容易被系統選擇執行(線程優先級并不是一項穩定的調節手段)。
3、狀態轉換
Java語言定義了6種線程狀態,在任意一個時間點中,一個線程只能有且只有其中的一個狀態,并且可以通過特定的方法在不同狀態之間轉換:
- 新建(New):創建后尚未啟動的線程處于這種狀態
- 運行(Runnable):包括操作系統線程狀態中的Running和Ready,也就是處于此狀態的線程有可能正在執行,也有可能正在等待著操作系統為它分配執行時間
- 無限期等待(Waiting):處于這種狀態的線程不會被分配處理器執行時間,它們要等待被其他線程顯式喚醒。以下方法會讓線程陷入無限期的等待狀態:沒有設置Timeout參數的Object::wait()方法;沒有設置Timeout參數的Thread::join()方法;LockSupport::park()方法。
- 限期等待(Timed Waiting):處于這種狀態的線程也不會被分配處理器執行時間,不過無須等待被其他線程顯式喚醒,在一定時間之后它們會由系統自動喚醒。以下方法會讓線程進入限期等待狀 態:Thread::sleep()方法;設置了Timeout參數的Object::wait()方法;設置了Timeout參數的Thread::join()方法;LockSupport::parkNanos()方法;LockSupport::parkUntil()方法
- 阻塞(Blocked):線程被阻塞了,“阻塞狀態”與“等待狀態”的區別是“阻塞狀態”在等待著獲取到一個排它鎖,這個事件將在另外一個線程放棄這個鎖的時候發生;而“等待狀態”則是在等待一段時 間,或者喚醒動作的發生。在程序等待進入同步區域的時候,線程將進入這種狀態
- 結束(Terminated):已終止線程的線程狀態,線程已經結束執行
五、Java與協程(了解即可)
1、內核線程的局限
目前Java線程面臨的困境:對Web應用的服務要求,不論是在請求數量上還是在復雜度上,與十多年前相比已不可同日而語,這一方面是源于業務量的增長,另一方面來自于為了應對業務復雜化而不斷進行的服務細分?,F代B/S系統中一次對外部業務請求的響 應,往往需要分布在不同機器上的大量服務共同協作來實現,這種服務細分的架構在減少單個服務復雜度、增加復用性的同時,也不可避免地增加了服務的數量,縮短了留給每個服務的響應時間。這要求每一個服務都必須在極短的時間內完成計算,這樣組合多個服務的總耗時才不會太長;也要求每一個服務提供者都要能同時處理數量更龐大的請求,這樣才不會出現請求由于某個服務被阻塞而出現等待。
Java目前的并發編程機制就與上述架構趨勢產生了一些矛盾,1:1的內核線程模型是如今Java虛擬機線程實現的主流選擇,但是這種映射到操作系統上的線程天然的缺陷是切換、調度成本高昂,系統能容納的線程數量也很有限。以前處理一個請求可以允許花費很長時間在單體應用中,具有這種線程切換的成本也是無傷大雅的,但現在在每個請求本身的執行時間變得很短、數量變得很多的前提下, 用戶線程切換的開銷甚至可能會接近用于計算本身的開銷,這就會造成嚴重的浪費
2、協程的復蘇
為什么內核線程調度切換成本高?
內核線程的調度成本主要來自于用戶態與內核態之間的狀態轉換,而這兩種狀態轉換的開銷主要來自于響應中斷、保護和恢復執行現場的成本
協程的主要優勢是輕量。在64位Linux上HotSpot的線程棧容量默認是1MB,此外內核數據結構還會額外消耗16KB內存。與之相對的,一個協程的棧通常在幾百個字節到幾KB之間,所以Java虛擬機里線程池容量達到兩百就已經不算小了,而很多支持協程的應用中,同時并存的協程數量可數以十萬計
協程的局限是需要在應用層面實現的內容特定多
3、Java的解決方案
六、附錄
總結
以上是生活随笔為你收集整理的深入理解Java虚拟机(周志明第三版)- 第十二章:Java内存模型与线程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 周志明jvm第三版笔记-第一部分:第一章
- 下一篇: 深入理解Java虚拟机(周志明第三版)-