《深刻理解 Java 虛擬機》讀書筆記:Java 內存模型與線程

正文

因爲計算機的處理器運算速度與它的存儲和通訊子系統速度的差距太大了,大量的時間都花費在磁盤 I/O、網絡通訊或者數據庫訪問上,致使處理器在大部分時間裏都處於等待其餘資源的狀態。所以,爲了充分利用計算機的處理器運算能力,現代計算機操做系統採用了多任務處理的方式,即讓計算機併發處理多個任務。java

對於計算量相同的任務,程序線程併發協調得越有條不紊,效率天然就會越高;反之,線程之間頻繁阻塞甚至死鎖,將會大大下降程序的併發能力。數據庫

1、硬件的效率與一致性

一、高速緩存

因爲計算機的存儲設備與處理器的運算速度有幾個數量級的差距,因此現代計算機系統加入了一層讀寫速度儘量接近處理器運算速度的高速緩存來做爲內存與處理器之間的緩衝:將運算須要使用的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步回內存中,這樣處理器就無須等待緩慢的內存讀寫了。數組

二、緩存一致性

基於高速緩存的存儲交互解決了處理器與內存的速度矛盾,但也引入了一個新的問題:緩存一致性。緩存

在多處理器系統中,每一個處理器都有本身的高速緩存,而它們又共享同一主內存。當多個處理器的運算任務都涉及同一塊主內存區域時,將可能致使各自的緩存數據不一致。爲了解決一致性的問題,須要各個處理器訪問緩存時遵循一些協議,在讀寫時根據協議來進行操做,好比 MSI、MESI 等協議。安全

處理器、高速緩存、主內存間的交互關係:網絡

三、亂序執行

除了增長高速緩存以外,爲了使處理器內部的運算單元能儘可能被充分利用,處理器可能會對輸入代碼進行亂序執行優化。處理器會在計算以後將亂序執行的結果重組,保證該結果與順序執行的結果一致。多線程

2、Java 內存模型

Java 內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。此處的變量包括實例字段、靜態字段和構成數組對象的元素,但不包括局部變量與方法參數,由於後者是線程私有的,不會被共享,不存在競爭問題。併發

一、主內存與工做內存

Java 內存模型規定了全部的變量都存儲在主內存中。每條線程還有本身的工做內存,線程的工做內存中保存了被該線程使用到的變量的主內存副本拷貝。函數

線程對變量的全部操做(讀取、賦值等)都必須在工做內存中進行,而不能直接讀寫主內存中的變量。不一樣的線程之間也沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞均須要經過主內存來完成。優化

從定義上來看,主內存主要對應於 Java 堆中的對象實例數據部分,而工做內存則對應於虛擬機棧中的部分區域。

從更低層次上說,主內存直接對應於物理硬件的內存,而爲了獲取更好的運行速度,虛擬機(甚至是硬件系統自己的優化措施)可能會讓工做內存優先存儲於寄存器和高速緩存中,由於程序運行時主要訪問讀寫的是工做內存。

二、內存間交互操做

關於主內存與工做內存之間的交互協議,即一個變量如何從主內存拷貝到工做內存、如何從工做內存同步回主內存之類的實現細節,Java 內存模型中定義了 8 種操做來完成,虛擬機必須保證每一種操做都是原子的、不可再分的。

  • lock(鎖定):做用於主內存的變量,它把一個變量標識爲一條線程獨佔的狀態。
  • unlock(解鎖):做用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定。
  • read(讀取):做用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工做內存中,以便隨後的 load 動做使用。
  • load(載入):做用於工做內存的變量,它把 read 操做從主內存中獲得的變量值放入工做內存的變量副本中。
  • use(使用):做用於工做內存的變量,它把工做內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個須要使用到變量的值的字節碼指令時將會執行這個操做。
  • assign(賦值):做用於工做內存的變量,它把一個從執行引擎接收到的值賦給工做內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做。
  • store(存儲):做用於工做內存的變量,它把工做內存中一個變量的值傳送到主內存中,以便隨後的 write 操做使用。
  • write(寫入):做用於主內存的變量,它把 store 操做從工做內存中獲得的變量的值放入主內存的變量中。

三、對於 volatile 型變量的特殊規則

volatile 的做用:

  • 保證變量對全部線程的可見性。即當一條線程修改了某個變量的值,新值對於其餘線程來講是能夠當即得知的。
  • 禁止指令重排序優化。

volatile 變量只能保證可見性,不能保證原子性。在如下運算場景中,仍然要經過加鎖(使用 synchronized 或 java.util.concurrent 中的原子類)來保證原子性:

  • 運算結果依賴於變量的當前值,而且其餘線程可能會修改變量的值。
  • 變量須要與其餘的狀態變量共同參與不變約束。

對 volatile 變量的特殊規則:

  • 某個線程對 volatile 變量的 read、load、use 操做必須連續一塊兒出現。這條規則要求在工做內存中,每次使用變量前都必須先從主內存刷新最新的值,用於保證能看見其餘線程對變量所作的修改後的值。
  • 某個線程對 volatile 變量的 assign、store、write 操做必須連續一塊兒出現。這條規則要求在工做內存中,每次修改變量後都必須馬上同步回主內存中,用於保證其餘線程能夠看到本身對變量所作的修改。
  • 若是線程 A 的 use、assign 操做先於線程 B,那麼線程 A 的 read、write 也必須先於 線程 B。這條規則要求 volatile 變量不會被指令重排序優化,保證代碼的執行順序與程序的順序相同。

四、對於 long 和 double 型變量的特殊規則

Java 內存模型容許虛擬機將沒有被 volatile 修飾的 64 位數據(long 和 double)的讀寫操做劃分爲兩次 32 位的操做來進行,即容許虛擬機實現能夠不保證 64 位數據類型的 load、store、read 和 write 這 4 個操做的原子性,這就是所謂的 long 和 double 的非原子性協定。

目前各平臺下的商用虛擬機幾乎都把 64 位數據的讀寫操做實現爲具備原子性的操做,所以在編寫代碼時通常不須要把 long 和 double 變量專門聲明爲 volatile。

五、原子性、可見性與有序性

Java 內存模型是圍繞着在併發過程當中如何處理原子性、可見性和有序性這 3 個特徵來創建的。

(1)原子性

  • 基本數據類型的訪問讀寫具有原子性(不考慮 long、double 的非原子性協定): Java 內存模型直接保證了 read、load、assign、use、store 和 write 操做的原子性。
  • synchronized 代碼塊之間的操做具有原子性:底層經過 lock 和 unlock 操做實現。

(2)可見性

可見性是指當一個線程修改了共享變量的值,其餘線程可以當即得知這個修改。

Java 內存模型是經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存做爲傳遞媒介的方式來實現可見性的。

volatile、synchronized、final 關鍵字都能實現可見性。

(3)有序性

Java 程序中自然的有序性能夠總結爲一句話:若是在本線程內觀察,全部的操做都是有序的;若是在一個線程中觀察另外一個線程,全部的操做都是無序的。前半句是指「線程內表現爲串行的語義」,後半句是指「指令重排序」現象和「工做內存與主內存同步延遲」現象。

Java 語言提供了 volatile、synchronized 關鍵字來保證線程之間操做的有序性。

六、先行發生原則

先行發生是 Java 內存模型中定義的兩項操做之間的偏序關係,若是說操做 A 先行發生於操做 B,其實就是說在發生操做 B 以前,操做 A 產生的影響能被操做 B 觀察到。「影響」包括修改了內存中共享變量的值、發送了消息、調用了方法等。它是判斷數據是否存在競爭、線程是否安全的主要依據。

Java內存模型的先行發生關係:

  • 程序次序規則:在一個線程內,按照程序代碼順序,書寫在前的操做先行發生於書寫在後的操做。準確地說,是控制流順序而不是程序代碼順序,由於要考慮分支、循環等結構。
  • 管程鎖定規則:一個 unlock 操做先行發生於後面(時間上的前後順序)對同一個鎖的 lock 操做。
  • volatile 變量規則:對一個 volatile 變量的寫操做先行發生於後面(時間上的前後順序)對這個變量的讀操做。
  • 線程啓動規則:Thread 對象的 start() 方法先行發生於此線程的每個動做。
  • 線程終止規則:線程中的全部操做都先行發生於對此線程的終止檢測,能夠經過 Thread.isAlive() 方法檢測到線程是否已經終止執行。
  • 線程中斷規則:對線程 interrupt() 方法的調用先行發生於被中斷線程檢測到中斷事件的發生,能夠經過 Thread.interrupted() 方法檢測到是否有中斷髮生。
  • 對象終結規則:一個對象的初始化完成(構造函數執行結束)先行發生於它的 finalize() 方法的開始。
  • 傳遞性:若是操做 A 先行發生於操做 B,操做 B 先行發生於操做 C,那麼能夠得出操做 A 先行發生於操做 C。

若是兩個操做之間的關係不知足以上規則,而且沒法從以上規則推導出來,那麼它們就沒有順序性保障,虛擬機能夠對它們隨意地進行重排序。

3、Java 與線程

一、線程的實現

主流的操做系統都提供了線程實現,Java 語言則提供了在不一樣硬件和操做系統平臺下對線程操做的統一處理,每一個已經執行 start() 且還未結束的 java.lang.Thread 類的實例就表明了一個線程。

(1)使用內核線程實現

內核線程(Kernel-Level Thread,KLT)就是直接由操做系統內核(Kernel)支持的線程,這種線程由內核來完成線程切換,內核經過操縱調度器對線程進行調度,並負責將線程的任務映射到各個處理器上。

每一個內核線程能夠視爲內核的一個分身,這樣操做系統就有能力同時處理多件事情,支持多線程的內核就叫作多線程內核。

程序通常不會直接使用內核線程,而是使用內核線程的一種高級接口——輕量級進程(Light Weight Process,LWP),輕量級進程就是一般意義上所講的線程,每一個輕量級進程都由一個內核線程支持。這種輕量級進程與內核線程之間 1:1 的關係稱爲一對一的線程模型

輕量級進程的侷限性:

  • 因爲是基於內核線程實現的,因此各類線程操做,如建立、析構及同步,都須要進行系統調用。而系統調用的代價相對較高,須要在用戶態和內核態中來回切換。
  • 每一個輕量級進程都須要有一個內核線程的支持,會消耗必定的內核資源(如內核線程的棧空間),所以一個系統支持輕量級進程的數量是有限的。

(2)使用用戶線程實現

用戶線程(User Thread,UT)徹底創建在用戶空間的線程庫上,系統內核不能感知線程的存在。用戶線程的創建、同步、銷燬和調度徹底在用戶態中完成,不須要內核的幫助。所以操做快速且低消耗,也能夠支持規模更大的線程數量。這種進程與用戶線程之間 1:N 的關係稱爲一對多的線程模型

使用用戶線程的優點在於不須要系統內核支援,劣勢也在於沒有系統內核的支援,全部的線程操做都須要用戶程序本身處理。所以使用用戶線程實現的程序通常都比較複雜。

(3)使用用戶線程加輕量級進程混合實現

混合實現時,用戶線程仍是徹底創建在用戶空間中,而操做系統提供支持的輕量級進程則做爲用戶線程和內核線程之間的橋樑。在這種混合模式中,用戶線程與輕量級進程的數量比是不定的,即爲 N:M 的關係,這種就是多對多的線程模型

混合實現的好處:

  • 用戶線程的操做依然廉價,而且能夠支持大規模的用戶線程併發。
  • 可使用內核提供的線程調度功能及處理器映射。
  • 因爲用戶線程的系統調用要經過輕量級進程來完成,所以大大下降了整個進程被徹底阻塞的風險。

二、Java 線程調度

線程調度是指系統爲線程分配處理器使用權的過程,主要調度方式有兩種:協同式線程調度和搶佔式線程調度。

(1)協同式線程調度

線程的執行時間由線程自己來控制,線程執行完以後,主動通知系統切換到另一個線程上。

協同式線程調度最大的好處是實現簡單,並且切換線程的操做對線程本身是可知的,因此沒有什麼線程同步的問題。它的壞處就是線程執行時間不可控,若是一個線程編寫有問題,一直不告知系統進行線程切換,那麼程序就會一直阻塞在那裏。

(2)搶佔式線程調度

每一個線程由系統來分配執行時間,線程的切換不禁線程自己來決定。

使用搶佔式線程調度時,線程的執行時間是系統可控的,不會有一個線程致使整個進程阻塞的問題。

Java 使用的線程調度方式就是搶佔式調度。

三、線程狀態

(1)6 種線程狀態

  • 新建(New):建立後還沒有啓動的線程處於這種狀態。
  • 運行(Runable):包括了操做系統線程狀態中的 Running 和 Ready,處於此狀態的線程有可能正在執行,也有可能正在等待着 CPU 爲它分配執行時間。
  • 無限期等待(Waiting):不會被分配 CPU 執行時間,等待着被其餘線程顯式地喚醒。
  • 限期等待(Timed Waiting):不會被分配 CPU 執行時間,無須等待被其餘線程顯式地喚醒,在必定時間以後會由系統自動喚醒。
  • 阻塞(Blocked):線程被阻塞了,在等待着獲取到一個排他鎖。在程序等待進入同步區域的時候,線程將進入這種狀態。
  • 結束(Terminated):已終止線程的線程狀態,線程已經結束執行。

(2)線程狀態轉換

相關文章
相關標籤/搜索