關於java線程模型
在Java中,基本我們說的線程(Thread)實際上應該叫作“用戶線程”,而對應到操作系統,還有另外一種線程叫作“內核線程”。
用戶線程和內核線程之間必然存在某種關系,多對一模型、一對一模型和多對多模型
多對一線程模型
多個用戶線程對應到同一個內核線程上,線程的創建、調度、同步的所有細節全部由進程的用戶空間線程庫來處理。
優點:用戶線程的很多操作對內核來說都是透明的,不需要用戶態和內核態的頻繁切換,使線程的創建、調度、同步等非常快;
缺點:由於多個用戶線程對應到同一個內核線程,如果其中一個用戶線程阻塞,那么該其他用戶線程也無法執行;
內核並不知道用戶態有哪些線程,無法像內核線程一樣實現較完整的調度、優先級等;
一對一模型
即一個用戶線程對應一個內核線程,內核負責每個線程的調度
優點:(比如JVM幾乎把所有對線程的操作都交給了內核)實現線程模型的容器(jvm)簡單,所以我們經常聽到在java中使用線程一定要慎重就是這個原因;
缺點:對用戶線程的大部分操作都會映射到內核線程上,引起用戶態和內核態的頻繁切換;
內核為每個線程都映射調度實體,如果系統出現大量線程,會對系統性能有影響;
多對多模型
本篇文章暫不作介紹
內核態、用戶態
如果有以上的認知,那么一個java的線程在運行的時候是內核態還是用戶態呢?
其實這是個偽命題,因為一個軟件級別的線程,用戶態和內核態是不確定的。那么什么是內核態什么用戶態呢?
這就要說到mmu和mmap了
linux系統的虛擬地址映射
一、物理地址空間是什么
理解虛擬地址空間還得從物理地址空間開始說起。我們知道內存就像一個數組,每個存儲單元被分配了一個地址,這個地址就是物理地址,所有物理地址構成的集合就是物理地址空間。物理地址也就是真實的地址,對應真實的那個內存條。
二、虛擬地址空間是什么
引入虛擬地址之后,對於每一個進程,操作系統提供一種假象,讓每個進程感覺自己擁有一個巨大的連續的內存可以使用,這個虛擬的空間甚至還可以比內存的容量還大。這個“假象”就是虛擬地址空間。虛擬地址是面向每個進程的,只是一個“假象”罷了。
CPU使用虛擬地址向內存尋址,通過專用的內存管理單元(MMU)硬件把虛擬地址轉換為真實的物理地址。
intel x86 CPU有四種不同的執行級別0-3,linux只使用了其中的0級和3級分別來表示內核態和用戶態,所謂的內核態和用戶態其實僅僅是CPU的一個權限而已。
用戶態切換到內核態的3種方式
a. 系統調用
b. 異常(這個異常不是java當中的異常)
c. 外圍設備的中斷
其實站在java程序員的角度只需要關注系統調用,因為系統調用可以認為是用戶進程主動發起的,比如:調用線程的park()方法會對應到一個os的一個函數,從而使當前線程進入了內核態;再比如遇到synchronized關鍵字如果是重量鎖則會調用pthread_mutex_lock()這樣我們的線程也會切換到內核態;當執行完系統調用切換到用戶態;
什么是切換?有哪些切換
而在每個任務運行前,CPU 都需要知道任務從哪里加載、又從哪里開始運行,也就是說,需要系統事先幫它設置好CPU
寄存器和程序計數器。
什么是 CPU 上下文
和spring上下文差不多,CPU 寄存器和程序計數器就是 CPU 上下文,因為它們都是 CPU 在運行任何任務前,必須的依賴環境。
- CPU 寄存器是 CPU 內置的容量小、但速度極快的內存。
- 程序計數器則是用來存儲 CPU 正在執行的指令位置、或者即將執行的下一條指令位置。
什么是 CPU 上下文切換
就是先把前一個任務的 CPU 上下文(也就是 CPU 寄存器和程序計數器)保存起來,然后加載新任務的上下文到這些寄存器和程序計數器,最后再跳轉到程序計數器所指的新位置,運行新任務。而這些保存下來的上下文,會存儲在系統內核中,並在任務重新調度執行時再次加載進來。這樣就能保證任務原來的狀態不受影響,讓任務看起來還是連續運行。
CPU 上下文切換的類型
根據任務的不同,和java並發編程相關的我們只關心以下兩種類型 - 進程上下文切換 - 線程上下文切換。
進程上下文切換
1、進程上下文切換之系統調用
進程既可以在用戶空間運行,又可以在內核空間中運行。進程在用戶空間運行時,被稱為進程的用戶態,而陷入內核空間的時候,被稱為進程的內核態。
從用戶態到內核態的轉變,需要通過系統調用來完成。比如,當我們查看文件內容時,就需要多次系統調用來完成:首先調用 open() 打開文件,然后調用 read() 讀取文件內容,並調用 write() 將內容寫到標准輸出,最后再調用 close() 關閉文件。
在這個過程中就發生了 CPU 上下文切換,整個過程是這樣的:
1)保存 CPU 寄存器里原來用戶態的指令位
2)為了執行內核態代碼,CPU 寄存器需要更新為內核態指令的新位置。
3)跳轉到內核態運行內核任務。
4)當系統調用結束后,CPU 寄存器需要恢復原來保存的用戶態,然后再切換到用戶空間,繼續運行進程。
所以,一次系統調用的過程,其實是發生了兩次 CPU 上下文切換
不過,需要注意的是,系統調用過程中,並不會涉及到虛擬內存等進程用戶態的資源,也不會切換進程。這跟我們通常所說的進程上下文切換是不一樣的:進程上下文切換,是指從一個進程切換到另一個進程運行;而系統調用過程中一直是同一個進程在運行。
所以,系統調用過程通常稱為特權模式切換,而不是上下文切換。系統調用屬於同進程內的 CPU 上下文切換。
2、真正的進程上下文切換和系統調用有什么區別呢?
進程的上下文不僅包括了虛擬內存、棧、全局變量等用戶空間的資源,還包括了內核堆棧、寄存器等內核空間的狀態。
因此,進程的上下文切換就比系統調用時多了一步:在保存內核態資源(當前進程的內核狀態和 CPU寄存器)之前,需要先把該進程的用戶態資源(虛擬內存、棧等)保存下來;而加載了下一進程的內核態后,還需要刷新進程的虛擬內存和用戶棧。
發生進程上下文切換的場景:
1)為了保證所有進程可以得到公平調度,CPU 時間被划分為一段段的時間片,這些時間片再被輪流分配給各個進程。這樣,當某個進程的時間片耗盡了,就會被系統掛起,切換到其它正在等待CPU 的進程運行。
2)進程在系統資源不足(比如內存不足)時,要等到資源滿足后才可以運行,這個時候進程也會被掛起,並由系統調度其他進程運行。
3)當進程通過睡眠函數 sleep 這樣的方法將自己主動掛起時,自然也會重新調度。
4)當有優先級更高的進程運行時,為了保證高優先級進程的運行,當前進程會被掛起,由高優先級進程來運行。
線程上下文切換
特點以及場景:
1. 前后兩個線程屬於不同進程。此時,因為資源不共享,所以切換過程就跟進程上下文切換是一樣。
2. 前后兩個線程屬於同一個進程。此時,因為虛擬內存是共享的,所以在切換時,虛擬內存這些資源就保持不動,只需要切換線程的私有數據、寄存器等不共享的數據和一個同學的套路(cas不會升級內核態,他僅僅是處理器提供的一個指令,速度非常快)