Java 執行緒實現方式
程序與執行緒
在傳統的作業系統中,最核心的概念是“程序”,程序是對正在執行的程式的一個抽象。程序的存在讓“並行”成為了可能,在一個作業系統中,允許執行著多個程序,這些程序“看起來”是同時在執行的。如果我們的計算機同時執行著 web 瀏覽器、電子郵件客戶端、即時通訊軟體例如QQ微信等多個程序,我們感覺這些程序都是同時在執行的,假設這臺計算機搭配的是多個 CPU 或者 多核 CPU,那麼這種多個程序並行的現象可能一點也不奇怪,完全可以為每個程序單獨分配一個 CPU,這樣就實現了多程序並行。然而事實上,在計算機只有一個 CPU 的情況下,它也能給人類一種感覺:多個程序同時在執行。但人類的感覺往往是比較模糊的,不精確的。事實是由於 CPU 的計算速度非常地快,它能快速地在各個程序之間切換,在某一瞬間,CPU 只能執行一個程序,但一秒鐘之內,它就能通過快速切換,讓人產生多個程序同時在執行的錯覺。在作業系統中,為什麼在程序的基礎上,又衍生出了執行緒的概念呢?
- 由於對於一些程序而言,它內部會發生多種活動,有些活動可能會在某個時間裡阻塞,有些活動不會,如果通過執行緒將這些活動分離開使它們能夠並行地執行,則設計程式的時候會更加簡單。
- 執行緒比程序的建立更加輕量級,效能消耗更少
- 如果一個程序既需要 CPU 計算,也需要I/O處理,擁有多執行緒允許這些活動重疊進行,加快整個程序的執行速度。
每一個程序在作業系統中都擁有獨立的一塊記憶體地址空間,該程序建立的所有執行緒共享這塊記憶體,支援多執行緒的作業系統,會讓執行緒作為 CPU 排程的最小單位。CPU 的時間片在不同的執行緒之間進行分配。
執行緒的可能實現方式
基本上主流的作業系統都支援執行緒,也提供了執行緒的實現。而 Java 語言為了應對不同硬體和作業系統的差異,提供了對執行緒操作的統一抽象,在 Java 中我們使用 Thread 類來代表一個執行緒。Thread 的具體實現可能會有不同的實現方式:
使用核心執行緒實現
核心執行緒是作業系統核心支援的執行緒,在核心中有一個執行緒表用來記錄系統中的所有執行緒,建立或者銷燬一個執行緒時,都需要涉及到系統呼叫,然後再核心中對執行緒表進行更新操作。對核心執行緒的阻塞以及其它操作,都涉及到系統呼叫,系統呼叫的代價都比較大,涉及到在使用者態和核心態之間的來回切換。此外,核心內部有執行緒排程器,用於決定應該將 CPU 時間片分配個哪個執行緒。程式一般不會直接操作核心執行緒,而是使用核心執行緒的一種高階介面,輕量級程序。輕量級程序與核心執行緒之間的關係是 1:1,每一個輕量級程序內部都有一個核心執行緒支援。
上圖中, LWP 指 Light Weight Process,即輕量級程序;KLT 指 Kernel Level Thread,即核心執行緒。
使用使用者執行緒實現
使用者執行緒是程式或者程式語言自己實現的執行緒庫,系統核心無法感知到這些執行緒的存在。使用者執行緒的建立、同步、銷燬和排程,都在使用者態中完成,無須核心的幫助,不需要進行系統呼叫,這樣的好處是對於執行緒的操作是非常高效的。在這種情況下,程序和使用者執行緒的比例是 1 :N。
使用者態執行緒面對如何阻塞執行緒時,會面臨困難,阻塞一個使用者態執行緒會出現把整個程序都阻塞的情況,多執行緒也就失去了意義。因為缺少核心的支援,所以很多需要利用核心才能完成的工作,例如阻塞與喚醒執行緒、多 CPU 環境下執行緒的對映等,都需要使用者程式去實現,實現起來會異常困難。
使用使用者執行緒和核心執行緒混合實現
在這種混合實現下,既存在使用者執行緒,也存在核心執行緒。使用者態執行緒的建立、切換這些操作依然很高效,並且使用者態實現的執行緒,比較容易加大執行緒的規模。需要作業系統核心支援的功能,則通過核心執行緒來做到,例如對映到不同的處理器上、處理執行緒的阻塞與喚醒以及核心執行緒的排程等。這種實現依然會使用到輕量級程序 LWP,它是使用者執行緒和核心執行緒之間的橋樑。
Java 執行緒的實現
在 JDK1.2 之前, Java 的執行緒是使用使用者執行緒實現的,在 JDK1.2 之後,Java 才採用作業系統原生支援的執行緒模型來實現,Java 執行緒模型的實現方式,主要取決於作業系統。對於 Oracle JDK 來說,在 Windows 和 Linux 上的執行緒模型是採用一對一的方式實現的,即一條 Java 執行緒對映到一條輕量級程序(核心執行緒)。而在 Solaris 平臺中,Java 則支援 1 :1 和 N : M 的執行緒模型。
執行緒的阻塞與等待
Java 中的執行緒的狀態有以下幾種:New,Runnable,Waiting, TimedWaiting, Blocked,Terminated。
- NEW:執行緒初建立,未執行
- RUNNABLE:執行緒正在執行,但不一定消耗 CPU
- BLOCKED:執行緒正在等待另外一個執行緒釋放鎖
- WAITING:執行緒執行了 wait, join, LockSupport.park() 方法
- TIMED_WAITING:執行緒呼叫了sleep, wait, join, LockSupport.parkNanos() 等方法,與 WAITING 狀態不同的是,這些方法帶有表示時間的引數。
其中 Blocked 和 Waiting 有個重要的區別是,阻塞(Blocked)狀態的執行緒在等待獲取一個排他鎖,例如執行緒在等待進入一個synchronized關鍵字包圍的臨界區時,就進入 Blocked 狀態。而 Waiting 狀態則是在等待被喚醒,或者等待一段時間。