概述
前面幾篇博客我係統的整理了 java 線程類 Thread 的使用、線程狀態及併發安全性問題。本篇博客我打算簡單的介紹一下 java 線程和操作系統線程之間的關聯,以及 java 線程如何被調度。
java 線程模型
本篇博客我打算分以下幾個模塊展開:
- 線程和進程
- 線程模型
- java 線程模型及調度方式
1、進程和線程
線程和進程的概念常常在操作系統中被提到,一般它們都是這樣被定義的:
- 進程是操作系統分配資源的基本單位
- 線程是 CPU 調度的基本單位
早期的操作系統中只有進程的概念,CPU 通過直接調用進程完成任務。任務的併發執行通過多進程實現。後來隨着計算機應用的越來越龐大,進程所佔據的資源也越來越多。CPU 爲了實現任務的併發執行,切換進程所帶來的的損耗也越來越大。爲了解決上述問題,操作系統引入了更輕量級的線程。
線程是比進程更輕量級的調度單位。通過線程,可以把進程中的資源分配和資源調度分開,所有線程共享進程資源,又獨立調度。關於線程和進程的關係我列出以下幾條:
- 線程在進程下運行
- 一個進程可以包含多個線程,至少包含一個線程
- 不同進程間數據不共享,不同線程間數據也不共享
- 同一進程下所有線程共享進程數據
- 調度進程會帶來更多的計算機資源消耗
- 進程間不會互相影響,進程中線程掛掉會導致整個進程掛掉
現如今主流的操作系統都實現了線程的概念,java 代碼中通過 Thread.start() 啓動一個線程。該方法底層也是通過調用 JNI 方法實現的,也就是說 java 創建線程也是根據底層平臺實現的。
2、線程模型
常見的線程模型有以下三種:
- 內核線程模型
- 用戶線程模型
- 混合線程模型
2-1、內核線程模型
內核線程模型即完全依賴操作系統內核線程來完成多線程併發。在此模型下,線程的調度通過內核來完成。系統內核通過將多個線程執行的任務映射到CPU上完成。
一般情況下應用程序不會直接調用內核線程, 而是採用內核線程向上提供的接口 輕量級進程。我們可以抽象的把輕量級進程理解爲線程,每個輕量級進程都會有一個內核線程與之一一對應。我們把這種輕量級進程比內核線程等於1:1的模型也稱爲 一對一線程模型。
下面我通過簡單抽象繪圖描述一下這種模型:
- LWP:Light Weight Process,即輕量級進程
- KIT:Kernel-Level Thread,即內核線程
- Thread Scheduler:線程調度器,它是一個操作系統服務,爲正在運行的內核線程分配 CPU 時間。
在這種模式下,用戶進程中某個線程(輕量級進程)的阻塞不會導致整個進程的阻塞,其他未阻塞的線程還可以被 CPU 調度,整個進程還可以正常工作。但內核線程也存在以下缺陷:
- 由於線程(輕量級進程)本質還是基於內核線程實現,因此各種線程的創建、同步等操作都需要採用系統調用,而系統調用本身代價比較大,需要線程在用戶態和內核態之間不斷切換。
- 由於每個線程(輕量級進程)都需要一個內核線程相對應,因此線程的創建需要耗費一定的內核資源,而內核資源本身較少,也就是說系統所能支持的線程數是有限的
2-2、用戶線程模型(存疑)
在內核線程模型中,我們提到了用戶態和內核態,這裏我簡單提一下這兩種狀態的概念:
- 用戶態:處於用戶態的線程只能訪問上層應用資源和代碼
- 內核態:處於內核態的線程可以訪問內核中的資源和代碼,還可以執行操作系統層面的函數
簡單來說,劃分用戶態和內核態主要是爲了防止任何線程都可以操作內核資源,導致整個系統崩潰。關於用戶態和內核態的介紹先提到這裏,後面我們專門出博客介紹。
根據用戶態和內核態的概念,線程也可以被劃分爲 用戶線程 和 內核線程。上文我們提到的輕量級進程從宏觀角度看也是一種用戶線程。但是輕量級進程本質還是基於內核線程實現,它的很多操作需要切換到內核態執行,因此它可以被看做是一種特殊的用戶線程。
從微觀角度看,用戶線程是指完全建立在用戶態的線程,它的創建、啓動、阻塞,停止等操作可以在用戶態獨立完成,不需要內核線程的幫助。如果處理的好的話,用戶線程永遠不需要切換爲內核線程。由於省去了線程狀態切換所帶來的消耗,這種用戶線程是非常高效的。又因爲線程在用戶態被創建,用戶態相比內核態所佔內存更大,因此可以支持更多的線程併發數量。這種進程比線程等於1:N的線程模型我們也稱爲 一對多線程模型。
下面我通過簡單抽象繪圖描述這種線程模型:
- UT:用戶線程,微觀角度上的用戶線程
用戶線程模型的優勢上面已經做過介紹,下面我們主要來看用戶線程模式的缺陷:
因爲沒有內核線程的原因,所有線程操作都需要用戶程序自己解決,像線程的創建、運行、阻塞、切換等都需要用戶自己實現。CPU 調度到進程後,進程內部線程的運行法則需要應用程序自己來維護。一般情況下,想要實現一套線程運行法則是非常困難的。因此大多數採用用戶線程模型的應用直接調用第三方線程框架來完成操作。
存疑:在整理這部分博客時,我一直在考慮用戶線程模型是否真的可以完全不借助內核線程,一個不借助內核線程的應用如何訪問內核資源,如果不能訪問內核資源的話,它又怎麼保證任務能順利完成。
關於這塊可能還有一種新猜想:這裏的一對多模型是指用戶進程中的所有線程對應某一個內核線程,而不是說幾乎不使用內核線程。再查閱很多博客及書籍後,關於這塊內容我越來越迷糊。暫時先標記存疑,後續如果有機會解決的話再修改。
2-3、混合線程模型
除了完全使用用戶線程或完全使用內核線程的實現方式外,還有一種將兩者混合的實現方式。在這種混合線程模型下,既有用戶線程,也有內核線程和輕量級進程。
下面我通過簡單抽象繪圖描述一下這種模型:
在混合線程模型中,用戶線程的操作都是建立在用戶空間中。因此線程的創建、切換,回收等操作不需要切換到內核態執行,並且支持大規模的線程併發執行。
混合線程模型中的輕量級進程作爲媒介連接用戶線程和內核線程,這樣可以使用內核提供的線程調度功能及處理器映射,此時用戶線程的系統調用通過內核線程來完成。
在混合線程模型中,一個 LWP 可以對接多個 UT,而每個用戶進程又包含多個 LWP。這種用戶線程比輕量級進程爲 N:M 的線程模型也被稱爲 混合線程模型。
3、java 線程模型及調度方式
在 Windows 和 linux 操作系統中,絕大多數 jvm 都採用一對一線程模型,即一個 java 線程對應一個內核線程,線程的創建、阻塞、回收等操作都通過調用 JNI 方法在底層平臺實現。
有了 java 線程的模型,我們再來看看 java 線程調度的規則。常見的調度規則有以下兩種:
-
協同式調度:線程執行完畢後通知操作系統切換到其他線程。也就是說線程本身可以確定自己所執行的時間,並且主動通知操作系統來結束執行。它最大的優點就是簡單,並且由於線程可控制,因此一般情況下不需要考慮同步問題。它的缺點也非常明顯:如果一個線程由於異常無法執行完畢,那麼它將永遠不會釋放CPU資源,其他線程也無法被調度。
-
搶佔式調度:線程的執行時間由操作系統統一分配。也就是說線程本身不能確定自己所執行的時間,時間片執行完或線程阻塞都會導致切換到其他線程。它最大優點就是即使線程存在異常,也不會導致該線程一直佔有CPU,以致於其他線程無法正常運轉。它的缺點也非常明顯:併發執行鎖帶來的線程安全性問題。
java 使用的線程調度方式是搶佔式調度。爲了儘可能增加對線程的控制,java 線程還引入了 yield() 方法和 線程優先級 的概念。
-
當線程調用 yield() 方法後,當前線程會釋放CPU資源,重新等待 CPU 調度。值得一提的是,執行 yield() 方法釋放 CPU 資源的線程也可能立即獲取到 CPU 資源繼續向下執行。
-
線程優先級是一個比較模糊的概念,並不能起到決定性作用。也就是說,優先級高的線程不一定先執行,優先級低的線程同樣也不一定後執行。線程優先級只是儘可能的讓優先級高的線程先執行。因爲 java 線程的一對一模型決定它是基於內核線程實現的,內核線程的調度次序由底層平臺決定,並不能完全契合java 優先級。
關於 java 線程狀態等概念已經通過其他博客詳細介紹,這裏不做過多贅述。