Java內存模型與線程

Java內存模型與線程

併發處理的廣泛應用是使得Amdahl定律代替摩爾定律成爲計算機性能發展源動力的根本原因,也是人類"壓榨"計算機運算能力的最有力武器.

  • Amdahl定律通過系統中並行化和串行化的比重來描述多處理器系統能獲得的運算加速能力,摩爾定律則用於描述處理器晶體管數量與運行效率之間的發展關係.

每秒事務處理數TPS是衡量一個服務性能好壞的重要指標之一,它代表着一秒內服務端平均能響應的請求總數,而TPS值與程序的併發能力又有非常密切的關係.

物理機的併發問題

基於高速緩存的存儲交互很好地解決了處理器與內存的速度矛盾,但是它也引入一個新的問題:緩存一致性,在多處理器系統中,每個處理器都有自己的高速緩存,而它們又共享同一主內存,當多個處理器的運算任務都涉及同一塊主內存區域時,將可能導致各自的數據不一致.

爲了使得處理器內部的運算單元能儘量被充分利用,處理器可能會對輸入代碼進行亂序執行優化,類似的,Java虛擬機的即時編譯器中也有類似的指令重排序優化.

Java內存模型

Java內存模型的主要目標是定義程序中各個變量的訪問規則,此處的變量與Java編程中的所說的變量有所區別,它包括了實例字段,靜態字段和構成數組對象的元素.

Java內存模型規定了所有的變量都存儲在主內存(可以類比物理內存,但僅僅是虛擬機內存的一部分)中,每條線程還有自己的工作內存(可以類比處理器高速緩存),線程的工作內存中保存了被該線程使用到的變量的主內存副本拷貝,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存中的變量.不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要通過主內存來完成.(volatile變量依然有工作內存的拷貝,但是由於它的順序性規定,所以看起來如同直接在主內存中讀寫訪問一般.)

Java內存模型中定義了以下8種操作來完成,虛擬機實現時必須保證下面提及的每一種操作都是原子的,不可再分的.

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

執行上述8種基本操作時必須滿足如下規則:

  1. 不允許read和load,store和write操作之一單獨出現.
  2. 不允許一個線程丟棄它的最近的assign操作.
  3. 不允許一個線程無原因(沒有進行任何assign操作)地把數據從線程的工作內存同步回主內存中.
  4. 對一個變量實施use,store操作之前,必須先執行過了assign和load操作.
  5. 一個變量在同一時刻只允許一條線程對其進行lock操作,但lock操作可以被同一條線程重複執行多次,多次執行lock後,只有執行相同次數的unlock操作,變量纔會被解鎖.
  6. 如果對一個變量執行lock操作,那將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重寫執行load或assign操作初始化變量的值.
  7. 如果一個變量事先沒有被lock操作鎖定,那就不允許對它執行unlock操作,也不允許去unlock一個被其他線程鎖定住的變量.
  8. 對一個變量執行lock操作之前,必須先把此變量同步回主內存中(執行store,write操作).

與以上規則等價的先行發生原則,用於確定一個訪問在併發環境下是否安全.

Java內存模型對volatile專門定義了一些特殊的訪問規則.當一個變量定義爲volatile之後,它將具備兩種特性,第一是保證此變量對所有線程的可見性,第二個語義是禁止指令重排序優化.

常見誤解:volatile變量對所有線程是立即可見的,對volatile變量所有的寫操作都能立刻反應到其他線程之中,所以基於volatile變量的運算在併發下是安全的.

volatile變量在各個線程的工作內存中不存在一致性問題(每次使用之前都要先刷新),但是Java裏面的運算並非原子操作,導致volatile變量的運算在併發下一樣是不安全的.

由於volatile變量只能保證可見性,在不符合以下兩條規則的運算場景中,我們仍然要通過加鎖(使用synchronized或java.util.concurrent中的原子類)來保證原子性.

  • 運算結果並不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值.
  • 變量不需要與其他的狀態變量共同參與不變約束.

普通的變量只能保證在該方法的執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操作的順序與程序代碼中的執行順序一致.而volatile關鍵字則可以避免指令重排序.

volatile變量的操作,在多個CPU訪問內存時,需要使用內存屏障,內存屏障的指令會引起別的CPU或者別的內核無效化其Cache,使得volatile變量的修改對其他CPU立即可見.

volatile變量讀操作的性能消耗與普通變量幾乎沒有什麼差別,但是寫操作可能會慢一些.

long和double是64位的數據類型,如果有多個線程共享一個併爲聲明爲volatil的long或double類型的變量,並且同時對它們進行讀取和修改操作,那麼某些線程可能會讀取到一個即非原值,也不是其他線程修改值的代表了"半個變量"的數值.(在目前商用Java虛擬機中不會出現)

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

  1. 原子性:大致認爲基本數據類型的訪問讀寫是具備原子性的.synchronized塊之間的操作也具備原子性.
  2. 可見性:可見性是指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改.除了volatile之外,synchronized和final也能實現可見性.final的可見性是指:被final修飾的字段在構造器中一旦初始化完成,並且構造器沒有把this的引用傳遞出去,那在其他線程中就能看見final字段的值.
  3. 有序性:如果在本線程內觀察,所有操作都是有序的(線程內表現爲串行);如果在一個線程中觀察另一個線程,所有操作都是無序的(指令重排序,工作內存與主內存同步延遲).volatile和synchronized可以保證線程之間操作的有序性.

先行發生:如果說操作A先行發生於操作B,其實就是說在發生操作B之前,操作A產生的影響能被操作B觀,"影響"包括修改了內存中共享變量的值,發送了消息,調用了方法等.

Java內存模型下一些"天然的"先行發生關係:

  • 程序次序規則:在一個線程內,安裝控制流順序,書寫在前的操作先行發生於書寫在後面的操作.
  • 管程鎖定規則:一個unlock操作先行發生於後面(時間上)對同一個鎖的lock操作
  • volatile變量規則:對一個volatile變量的寫操作先行發生於後面(時間上)對這個變量的讀操作.
  • 線程啓動規則:Thread對象的start()方法先行發生於此線程的每一個動作.
  • 線程終止規則:線程中的所有操作都先行發生於對此線程的終止檢測.
  • 線程中斷規則:對線interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生.
  • 對象終結規則:一個對象的初始化完成(構造函數執行結束)先行發生於它的finalize()方法的開始.
  • 傳遞性:如果操作A先行發生於操作B,操作B先行發生於操作C,那就可以得出操作A先行發生於操作C的結論.

時間先後順序與先行發生原則之間基本沒有太大的關係,所以我們衡量併發安全問題的時候不要受到時間順序的干擾,一切必須以先行發生原則爲準.

Java與線程

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

實現線程主要有3種方式:

  1. 使用內核線程實現.
  2. 使用用戶線程實現.
  3. 使用用戶線程輕量級進程混合實現.

使用內核線程實現

內核線程(KLT)就是直接由操作系統內核支持的線程,這種線程由內核來完成線程切換,內核通過操縱調度器對線程進行調度,並負責將線程的任務映射到各個處理器上.每個內核線程可以視爲內核的一個分身,這樣操作系統就有能力同時處理多件事情,支持多線程的內核就叫多線程內核.

程序一般不會直接去使用內核線程,而是去使用內核線程的一種高級接口----輕量級進程(LWP),輕量級進程就是我們通常意義上所講的線程.由於每個輕量級進程都由一個內核線程支持,因此只有先支持內核線程,纔能有輕量級進程.

輕量級進程與內核線程之間1:1的關係:

優點:即使有一個輕量級進程在系統調用中阻塞了,也不會影響整個進程繼續工作.

缺點:基於內核線程實現,所以各種操作(創建,析構,同步),都需要進行系統調用,而系統調用的代價較高,需要在用戶態和內核態中來回切換.每個輕量級進程都需要有一個內核線程的支持,因此輕量級進程要消耗一定的內核資源(如內核線程的棧空間),因此一個系統支持輕量級進程的數量是有限的.

使用用戶線程實現

廣義上將,一個線程只要不是內核線程,就可以認爲是用戶線程,從這個定義講,輕量級進程也屬於用戶線程,但輕量級進程的實現始終是建立在內核之上的,許多操作都要進行系統調用,效率會受到限制.

狹義上的用戶線程指的是完全建立在用戶空間的線程庫上,系統內核不能感知線程存在的實現.用戶線程的建立,同步,銷燬和調度完全在用戶態中完成,不需要內核的幫助.如果程序實現得當,這種線程不需要切換到內核態,因此操作可以是非常快速且低消耗的,也可以支持規模更大的線程數量.

進程與用戶線程之間的1:N的關係:

優點:不需要內核支持,無需系統調用,無需從用戶態切換至核心態.快速且低消耗,能支持規模更大的線程數量

缺點:缺少內核支持,一個用戶線程如果阻塞在系統調用中,整個進程都將會被阻塞,且多處理器系統中幾乎無法將線程映射到其他處理器上.實現複雜.

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

在這種混合實現下,既存在用戶線程,也存在輕量級進程.用戶線程還是完全建立在用戶空間中,因此用戶線程的創建,切換,析構等操作依然廉價,並且可以支持大規模的用戶線程併發.而操作系統提供支持的輕量級進程則作爲用戶線程和內核線程之間的橋樑,這樣可以使用內核提供的線程調度功能處理器映射,並且用戶線程的系統調用通過輕量級進程來完成,大大降低了整個進程被阻塞的風險.在這種模式中,用戶線程與輕量級進程的數量比是不定的,即爲N:M的關係.

Java線程的實現

Java中的線程模型基於操作系統原生線程模型來實現的.對於Sun JDK來說,它的Windows版Linux版都是使用一對一的線程模型實現的,一條Java線程就映射到一條輕量級進程之中,因爲Windows和Linux系統提供的線程模型就是一對一的.

  • Windows下有纖程包,Linux也有NGPT來實現N:M模型,但是它們都沒有成爲主流.

而在Solaris平臺中可以同時支持一對一多對多的線程模型,因此在Solaris版的JDK中也對應提供了兩個平臺專有的虛擬機參數來明確指定虛擬機使用哪種線程模型.

Java線程調度

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

使用協同式調度的多線程系統,線程的執行時間由線程本身來控制,線程把自己的工作執行完成之後,要主動通知系統切換到另一個線程上,這種方式基本沒有什麼線程同步問題,缺點是線程執行時間不可控制,程序可能會一直阻塞在那裏.

如果使用搶佔式調度的多線程系統,那麼每個線程將由系統來分配執行時間,線程的切換不由線程本身來決定.也不會有一個線程導致整個進程阻塞的問題.

Java語言一共設置了10個級別線程優先級,在多個線程同時處於Ready狀態時,優先級越高的線程越容易被系統選擇執行.

狀態轉換

Java語言定義了幾種線程狀態,在任意一個時間點,一個線程只能有且只有其中的一種狀態,分別如下:

新建(New):創建後尚未啓動的線程處於這種狀態.

運行(Runable):Runable包括了操作系統進程狀態中的新建和就緒,也就是處於此狀態的線程有可能正在執行,也有可能正在等待CPU爲它分配執行時間.

無限期等待(Waiting):處於這種狀態的線程不會被分配CPU執行時間,他們要等待被其他線程顯式地喚醒.以下方法會讓線程陷入無限期的等待狀態:

  • 沒有設置Timeout參數的Object.wait()方法.
  • 沒有設置Timeout參數的Thread.join()方法.
  • LockSupport.park()方法.

限期等待(Timed Waiting):處於這種狀態的線程也不會被分配CPU執行時間,不過無須等待被其他線程顯式地喚醒,在一定時間之後它們會由系統自動喚醒.以下方法會讓線程進入限期等待狀態:

  • Thread.sleep()方法.
  • 設置了Timeout參數的Object.wait()方法.
  • 設置了Timeout參數的Thread.join()方法.
  • LockSupport.parkNanos()方法.
  • LockSupport.parkUntil()方法.

阻塞(Blocked):線程被阻塞了,阻塞狀態和等待狀態的區別是:阻塞狀態在等待着獲取到一個排他鎖,這個事件將在另一個線程放棄這個鎖的時候發生;而等待狀態則是在等待一段時間,或者喚醒動作的發生.在程序等待進入同步區域的時候,線程將進入這種狀態.

結束(Terminated):已終止線程的線程狀態,線程已經結束執行.

  • 注意與操作系統中進程三態模型五態模型區分.

進程的三態模型:

  1. 就緒
  2. 運行
  3. 阻塞

進程的五態模型:

  1. 新建
  2. 就緒
  3. 運行
  4. 阻塞
  5. 終止

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