JUC多线程:系统调用、进程、线程的上下文切换
一、什么是上下文切換:
1、內核空間的用戶空間:
? ? ? ? 講上下文切換前我們先了解下什么是內核空間和用戶空間,程序運行在內核空間的狀態稱為內核態,運行在用戶空間的狀態稱為用戶態,用戶態和內核態是操作系統的兩種運行狀態,劃分為這兩種空間狀態主要是為了對應用程序的訪問能力進行限制,防止應用程序隨意進行一些危險的操作導致系統崩潰,比如設置時鐘、內存清理,這些都需要在內核態下完成:
- ① 內核態:內核態運行的程序可以不受限制地訪問計算機的任何數據和資源,比如外圍設備網卡、硬盤等。處于內核態的 CPU 可以從一個程序切換到另外一個程序,并且所占用的 CPU 不會發生搶占情況。
- ② 用戶態:用戶態運行的程序只能受限地訪問內存空間,只能直接讀取用戶程序的數據,不允許訪問外圍設備網卡、硬盤等,用戶態下所占有的 CPU 會被其他程序搶占,不允許獨占。
????????如果應用程序需要使用到內核空間的資源,則需要通過系統調用來完成,從用戶空間切換到內核空間,直到完成相關的操作后再切合用戶空間,兩種狀態間的切換,就涉及到 CPU 的上下文切換
2、系統調用的 CPU 上下文切換:
????????系統調用過程中是如何發生 CPU 上下文的切換的呢?我們再了解兩個概念:
- ① CPU 寄存器,是 CPU 內置的容量小、但速度極快的內存。
- ② 程序計數器,則是用來存儲 CPU 正在執行的指令位置以及即將執行的下一條指令位置。
????????CPU 寄存器和程序計數器都是 CPU 在運行任何任務時必須的依賴環境,因此也被叫做 CPU 上下文。而 CPU 上下文切換,就是先把前一個任務的 CPU 上下文保存起來,然后加載新任務的上下文到這些寄存器和程序計數器,最后再跳轉到程序計數器所指的新位置,運行新任務。而這些保存下來的上下文,會存儲在系統內核中,并在任務重新調度執行時再次加載進來。這樣就能保證任務原來的狀態不受影響,讓任務看起來還是連續運行。
????????回到系統調用的問題上,為了切換到內核態,需要先保存 CPU 寄存器中用戶態的指令位置,然后更新 CPU 寄存器為內核態指令的新位置,最后跳轉到內核態運行內核任務。而系統調用結束后,CPU 寄存器需要恢復原來保存的用戶態,然后再切換到用戶空間,繼續運行進程。所以,一次系統調用的過程,其實是發生了兩次 CPU 上下文切換。
3、進程的上下文切換:
????????系統調用的上下文切換跟我們常說的進程上下文切換是不一樣的,進程上下文切換是指從一個進程切換到另一個進程運行,而系統調用過程中一直是同一個進程在運行,不會切換進程。
????????那么兩者的具體區別呢?首先,進程都是由內核來管理和調度的,進程的切換只能發生在內核態。所以,進程的上下文切換不僅包括內核堆棧、寄存器等內核空間的狀態,還包括了虛擬內存、棧、全局變量等用戶空間的資源。因此進程的上下文切換就比系統調用導致的上下文切換多了一個步驟,在保存當前進程的內核狀態和 CPU 寄存器之前,需要先把該進程的虛擬內存、棧等保存下來,等加載了下一個進程的內核態后,還需要刷新進程的虛擬內存和用戶棧。
4、線程上下文切換:
????????線程與進程最大的區別在于,進程是資源分配的基本單位,而線程是調度的基本單位,內核中的任務調度,實際上的調度對象是線程;同一個進程中的所有線程共享進程的虛擬內存、全局變量等資源。
????????在處理多線程并發任務時,處理器會給每個線程分配CPU時間片,線程在各自分配的時間片內占用處理器并執行任務,當線程的時間片用完了,或者自身原因被迫暫停運行的時候,就會有另外一個線程來占用這個處理器,這種一個線程讓出處理器使用權,另外一個線程獲取處理器使用權的過程就叫做上下文切換。
?????????一個線程讓出CPU處理器使用權,就是“切出”;另外一個線程獲取CPU處理器使用權,就是“切入”,在這個切入切出的過程中,操作系統會保存和恢復相關的進度信息,而這個進度信息就是我們常說的“上下文”,也就是我們上文提到的 CPU寄存器以及程序計數器。
????????這么一來,線程的上下文切換就可以分為兩種情況:
- ① 前后兩個線程屬于同一個進程。此時,因為共享虛擬內存,所以在切換時,虛擬內存、全局變量這些資源就保持不動,只需要切換線程的私有數據,比如棧和寄存器等不共享的數據。
- ②?前后兩個線程屬于不同進程。此時,因為資源不共享,所以切換過程就跟進程的上下文切換一樣,不僅包括了虛擬內存、棧、全局變量等用戶空間的資源,還包括了內核堆棧、寄存器等內核空間的狀態的修改
????????所以雖然同為線程的上下文切換,但同進程內的線程切換,要比多進程間的切換消耗更少的資源,而這正是多線程代替多進程的一個優勢。但在并發編程中,并不是線程越多就效率越高,線程數太少可能導致資源不能充分利用,線程數太多可能導致競爭資源激烈,導致上下文切換頻繁造成系統的額外開銷,因為上下文的保存和恢復過程是有成本的,需要內核在 CPU 上完成,每次切換都需要幾十納秒到數微秒的 CPU 時間,在進程上下文切換次數較多的情況下,很容易導致 CPU 將大量時間耗費在寄存器、內核棧以及虛擬內存等資源的保存和恢復上。
? ? ? ? 所以在單個邏輯比較簡單而且速度相對來說非常快的情況下,我們推薦是使用單線程。如果邏輯非常復雜,或者需要進行大量的計算的地方,我們建議使用多線程來提高系統的性能。
對于單核單線程 CPU 而言,在某一時刻只能執行一條 CPU 指令。從用戶角度看,計算機能夠并行運行多個進程,這恰恰是操作系統通過快速上下文切換造成的結果。每個時間片的大小一般為幾十毫秒,所以在一秒鐘就可能發生幾十上百次的線程相互切換,給我們的感覺就是同時進行的
5、Java 多線程的上下文切換:
????????Java 中多線程的上下文切換就是線程兩個運行狀態(Java 線程的狀態介紹見文章的第二部分)的相互切換導致的,在切換時操作系統保存的上下文信息,當線程從 BLOCKED 狀態進入到 RUNNABLE 時,也就是線程的喚醒,此時線程將獲取上次保存的上下文信息,接著之前的進度繼續執行。
????????在 Java 中有兩種情況會導致線程上下文切換:一種是自發性上下文切換,也就是程序本身觸發的切換;另一種是非自發性上下文切換,也就是系統或者虛擬機導致的上下文切換。
(1)自發性上下文是線程由 Java 程序調用導致切出,一般是在編碼的時候,調用以下幾個方法或關鍵字:
sleep()、wait()、yield()、join()、park()、synchronized、lock
(2)非自發的上下文切換常見的有:線程被分配的時間片用完,虛擬機垃圾回收導致,或者執行優先級的問題導致
文章看到這里,比較細心的讀者可能會有一個疑問:Java線程與操作系統線程的關系?對這個問題比較感興趣的讀者可以閱讀這篇文章:https://blog.csdn.net/a745233700/article/details/109410376
6、上下文切換的系統開銷:
上面介紹了幾種不同的上下文切換,那么上下文切換會造成什么問題呢?大致如下:
- 操作系統保存和恢復上下文
- 處理器高速緩存加載
- 調度器進行調度
- 上下文切換可能導致的高速緩沖區被沖刷
二、Java 線程的狀態:
1、線程狀態說明:
????????在 JDK 的 java.lang.Thread.State 源碼中定義了6個狀態,在某一時刻,一個線程只能處于一種狀態:
- New:新創建且尚未啟動的線程狀態。
- Runnable:可運行狀態,已獲得除CPU以外的所需的一切資源,等待CPU調度。
- Blocked:阻塞狀態,線程阻塞等待監視器鎖定,處于 synchronized 同步代碼塊或方法中被阻塞。
- Waiting:等待狀態。下列不帶超時的方式:Object.wait()、Thread.join()、 LockSupport.park()
- Timed Waiting:超時等待狀態,指定了等待時間的線程狀態。下列帶超時的方式:Thread.sleep()、0bject.wait()、 Thread.join()、 LockSupport.parkNanos()、 LockSupport.parkUntil()
- Terminated:線程終止狀態,線程正常完成執行或出現異常
?(1)New:實現 Runnable 接口和繼承 Thread 類可以得到一個線程類,當線程實例被 new 創建后,就進入了 NEW 狀態,但是此時線程還沒有開始執行,
(2)RUNNABLE:標識線程已經準備就緒,等待CPU調度,此時還是還沒真正執行。轉換成該狀態的條件:
-
① 當調用線程的 start() 方法時,線程也不一定會馬上執行,因為 Java 線程是映射到操作系統的線程執行的,還需要等CPU調度,但此時該線程的狀態已經為 RUNNABLE
-
② 當前線程時間片用完
-
③ Thread.yield():當前線程調用 yield() 方法,放棄所獲取的 CPU 時間片,由運行狀態變會可運行狀態,讓同優先級的線程執行,但不保證一定能達到讓步的目的,因為讓步的線程有可能被線程調度程序再次選中。
-
④ join() 的線程結束:在當前線程里調用線程A的 join() 方法,當前線程阻塞但不釋放對象鎖,直到線程A執行完畢 或者 millis 時間到,當前線程進入可運行狀態。
-
⑤ Thread.sleep(long millis):當前線程調用 sleep() 方法,當前線程進入阻塞但不釋放對象鎖,millis 后線程自動蘇醒進入可運行態。
-
⑥ 鎖池里的線程拿到對象鎖后,進入可運行狀態
(3)WAIT 等待:等待狀態,處于等待狀態的線程正在等待另一個線程執行特定操作。例如:
- Object.wait():一個在對象上調用 Object.wait() 的線程正在等待另一個線程在該對象上調用 Object.notify() 或 Object.notifyAll(),這樣便可以控制線程的執行順序。
- Thread.join():線程正在等待指定的 join 線程終止
- LockSupport.park()
2、java.lang.Thread.State 源碼:
public enum State {/*** Thread state for a thread which has not yet started.*/NEW,/*** Thread state for a runnable thread. A thread in the runnable* state is executing in the Java virtual machine but it may* be waiting for other resources from the operating system* such as processor.*/RUNNABLE,/*** Thread state for a thread blocked waiting for a monitor lock.* A thread in the blocked state is waiting for a monitor lock* to enter a synchronized block/method or* reenter a synchronized block/method after calling* {@link Object#wait() Object.wait}.*/BLOCKED,/*** Thread state for a waiting thread.* A thread is in the waiting state due to calling one of the* following methods:* <ul>* <li>{@link Object#wait() Object.wait} with no timeout</li>* <li>{@link #join() Thread.join} with no timeout</li>* <li>{@link LockSupport#park() LockSupport.park}</li>* </ul>** <p>A thread in the waiting state is waiting for another thread to* perform a particular action.** For example, a thread that has called <tt>Object.wait()</tt>* on an object is waiting for another thread to call* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on* that object. A thread that has called <tt>Thread.join()</tt>* is waiting for a specified thread to terminate.*/WAITING,/*** Thread state for a waiting thread with a specified waiting time.* A thread is in the timed waiting state due to calling one of* the following methods with a specified positive waiting time:* <ul>* <li>{@link #sleep Thread.sleep}</li>* <li>{@link Object#wait(long) Object.wait} with timeout</li>* <li>{@link #join(long) Thread.join} with timeout</li>* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>* </ul>*/TIMED_WAITING,/*** Thread state for a terminated thread.* The thread has completed execution.*/TERMINATED;}參考文章:
https://blog.csdn.net/qq_38862257/article/details/109685644
https://javaedge.blog.csdn.net/article/details/115721845
總結
以上是生活随笔為你收集整理的JUC多线程:系统调用、进程、线程的上下文切换的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 服务容错设计:流量控制、服务熔断、服务降
- 下一篇: 如何设计一个短URL地址系统