深入理解JVM(③)執行緒與Java的執行緒

紀莫發表於2020-07-11

前言

我們都知道,執行緒是比程式更輕量級的排程執行單位,執行緒的引入,可以把一個程式的資源分配和執行排程分開,各個執行緒既可以共享程式資源排程(記憶體地址、檔案I/O等),又可以獨立排程。

執行緒的實現

主流的作業系統都提供了執行緒實現,Jav語言則是提供了在不同硬體和作業系統平臺下對執行緒操作的統一處理,每個已經呼叫過start()方法且還未結束的java.lang.Thread類的例項就代表這一個執行緒。
其實Thread類與大部分的Java類庫API有著顯著差別,它的所有關鍵方法都被宣告為Native。在Java中,一個Native方法往往就意味著這個方法沒有使用或無法使用平臺無關的手段來實現(通常最高效率的手段就是平臺相關的手段)。
那麼執行緒的實現其實是有三種方式的:

  • 使用核心執行緒實現(1:1實現)
  • 使用使用者執行緒實現(1:N)實現
  • 使用使用者執行緒加輕量級程式混合實現

核心執行緒實現

使用核心執行緒實現的方式被稱為1:1實現。核心執行緒(Kernel Levvel Thread,KLT)就是直接由作業系統核心(Kernel,下稱核心)支援的執行緒,核心通過操縱排程器(Scheduler)對執行緒進行排程,並負責將執行緒的任務對映到各個處理器上。
其實程式一般不會直接使用核心執行緒,而是使用核心執行緒的一種高階介面——輕量級程式(Light Weight Process,LWP),輕量級程式就是我們通常所講的執行緒。這種輕量級程式與記憶體執行緒之間1:1的關係稱為一對一的執行緒模型
輕量級程式與核心執行緒之間1:1關係
輕量級程式也具有它的侷限性:首先,由於是基於核心執行緒實現的,所以各種執行緒操作(建立、析構及同步),都需要進行系統呼叫。系統呼叫就要在使用者態和核心態中來回切換。其次,每個輕量級程式都需要一個核心執行緒的支援,因此需要消耗一定的核心資源,所以一個系統支援輕量級程式的數量是有限的。

使用者執行緒實現

使用使用者執行緒實現的方式被稱為1:N實現。廣義上來講,一個執行緒只要不是核心執行緒,都可以任務是使用者執行緒(User Threa,UT)的一種。從定義上來看輕量級程式不是核心執行緒也就是屬於使用者執行緒,但是它始終是建立在核心之上的,所以效率會受到限制,並不具備使用者執行緒的優點。

使用者執行緒的建立、同步、銷燬和排程完全咋使用者態中完成,不需要核心幫助。如果程式實現得當,不需要切換核心態,因此操作可以是非常快且低消耗的,也能夠支援規模更大的執行緒數量,部分高效能資料庫中的多執行緒就是由使用者執行緒實現的。
這種程式與使用者執行緒之間1:N的關係稱為一對多的執行緒模型
程式與使用者執行緒1:N的關係
使用者執行緒的速度快低消耗等優勢在於不需要系統核心支援,但是劣勢也在於沒有核心的支援,所有的執行緒操作都需要由使用者程式自己去處理。這樣就會導致執行緒的一些問題處理起來就很困難,甚至有些是不可能實現的。
Java、Ruby等予以都曾經使用過使用者執行緒,最終又都放棄了使用它。

混合實現

執行緒除了依賴核心執行緒實現和完全由使用者程式自己實現之外,還有一種將核心執行緒與使用者執行緒一起使用的實現方式,被稱為N:M實現
使用者執行緒還是完全建立在使用者空間中,因此使用者執行緒的建立、切換、析構等操作依然廉價,並且可以支援大規模的使用者執行緒併發。而作業系統支援的輕量級程式則作為使用者執行緒和核心執行緒之間的橋樑,這樣可以使用核心提供的執行緒排程功能及處理器對映,並且使用者執行緒的系統呼叫要通過輕量級程式來完成,大大降低了整個程式被完全阻塞的風險。
在這裡插入圖片描述

Java執行緒的實現

Java執行緒如何實現並不受Java虛擬機器規範約束,這是一個與具體虛擬機器相關的畫圖。Java執行緒在早期的Classic虛擬機器上(JDK1.2以前),是基於一種被稱為“綠色執行緒”(Green Threads)的使用者執行緒實現的,但從JDK1.3起,“主流”平臺上的“主流”商用Java虛擬機器的執行緒模型普遍都被替換為基於作業系統原生執行緒模型來實現,即採用1:1的執行緒模型。
作業系統支援怎樣的執行緒模型,在很大程度想會影響上面的Java虛擬機器的執行緒是怎麼樣對映的,這一點咋不同的平臺上很難達成一致,因此《Java虛擬機器規範》中才不去限定Java執行緒需要使用哪種執行緒模型來實現。

Java執行緒排程

執行緒排程是指系統為執行緒分配處理使用權的過程,排程主要方式有兩種,分別是協同式(Cooperative Threads-Scheduling)執行緒排程搶佔式(Preemptive Threads-Scheduling)執行緒排程

  • 協同式執行緒排程:執行緒的執行時間由執行緒本身來控制,執行緒把自己的工作執行完了之後,要主動通知系統切換到另外一個執行緒上去。
    優點:實現簡單,切換操作對執行緒自己是可知的,所以一般沒有什麼執行緒同步問題。
    缺點:執行緒執行時間不可控制,甚至如果一個執行緒的程式碼編寫有問題,一直不告知系統進行執行緒切換,那麼程式就會一直阻塞在那裡。
  • 搶佔式執行緒排程:每個執行緒將由系統來分配執行時間,執行緒的切換不由執行緒本身來決定。
    優點:可以主動讓出執行時間(例如Java的Thread::yield()方法),並且執行緒的執行時間是系統可控的,也不會有一個執行緒導致整個系統阻塞的問題。
    缺點:無法主動獲取執行時間。

Java使用的就是搶佔式執行緒排程,雖然這種方式的執行緒排程是系統自己的完成的,但是我們可以給作業系統一些建議,就是通過設定執行緒優先順序來實現。Java語言一共設定了10個級別的執行緒優先順序。在兩個執行緒同時處於Ready狀態時,優先順序越高的執行緒越容易被系統選擇執行。

不過由於各個系統的提供的優先順序數量不一致,所以導致Java提供的10個級別的執行緒優先順序並不見得能與各系統的優先順序都一一對應

Java執行緒狀態轉換

Java語言定義了6種執行緒狀態,在任意一個時間點鐘,一個執行緒只能有且只有其中的一種狀態,並且可以通過特定的方法在不同狀態之間切換。

  • 新建(New):建立後尚未啟動的執行緒處於這種狀態。
  • 執行(Runnable):包括作業系統執行緒狀態中的Running和Ready,也就是處理此狀態的執行緒有可能正在執行,也有可能正在等待著作業系統為它分配執行時間。
  • 無限期等待(Waiting):處於這種狀態的執行緒不會被分配處理器執行時間,它們要等待被其他執行緒顯示喚醒。
    以下方法會讓執行緒陷入無限期等待狀態:
    1、沒有設定Timeout引數的Object::wait()方法;
    2、沒有設定Timeout引數的Thread::join()方法;
    3LockSupport::park()方法。
  • 限期等待(Timed Waiting):處於這種狀態的執行緒也不會被分配處理器執行時間,不過無須等待被其他執行緒顯式喚醒,在一定時間之後它們會由系統自動喚醒。
    以下方法會讓執行緒進入限期等待狀態:
    1Thread::sleep()方法;
    2、設定了Timeout引數的Object::wait()方法;
    3、設定了Timeout引數的Thread::join()方法;
    4LockSupport::parkNanos()方法;
    5LockSupport::parkUntil()方法;
  • 阻塞(Blocked):執行緒被阻塞了,“阻塞狀態”與“等待狀態”的區別是“阻塞狀態”在等待著獲取到一個排他鎖,這個事件將在另外一個執行緒放棄這個鎖的時候發生;而“等待狀態”則是在等待一段時間 ,或者喚醒動作發生。在程式進入同步區域的時候,執行緒將進入這種狀態。
  • 結束(Terminated):已終止執行緒的執行緒狀態,執行緒已經結束執行。

這6種狀態在遇到特定事件發生的時候將會互相轉換,他們的轉換關係如下圖:
執行緒狀態轉換關係

相關文章