天天看點

一文帶你讀懂:系統線程模型與實作原理

各種作業系統均提供了線程的實作(核心線程),線程是 CPU 進行工作排程的基本機關。線程是比程序更輕量級的排程執行機關,線程的引入,可以把一個程序的資源配置設定和執行排程分開,各個線程既可以共享程序資源(記憶體位址、檔案I/O等),又可以獨立排程(線程是CPU排程的基本機關)。而程式設計語言一般都會提供操作核心線程的 API, Java 也不例外。操作核心線程的模型主要有如下三種:

  1. 使用核心線程(1:1 模型)
  2. 使用使用者線程(1:N 模型)
  3. 使用使用者線程 + 輕量級程序(LWP)(N:M 模型)
一文帶你讀懂:系統線程模型與實作原理

基礎概念複習

我們先複習下作業系統中的幾個關鍵概念:

  • 核心線程 KLT:核心級線程(Kemel-Level Threads, KLT 也有叫做核心支援的線程),直接由作業系統核心支援,線程建立、銷毀、切換開銷較大
  • 使用者線程 UT:使用者線程(User Thread,UT),建立在使用者空間,系統核心不能感覺使用者線程的存在,線程建立、銷毀、切換開銷小
  • 輕量級程序 LWP:(LWP,Light weight process)使用者級線程和核心級線程之間的中間層,是由作業系統提供給使用者的操作核心線程的接口的實作 。
  • 程序 P:使用者程序
一文帶你讀懂:系統線程模型與實作原理

作業系統的三種線程模型

下面依次介紹三種線程模型:

  • 核心線程模型:

    核心線程模型即完全依賴作業系統核心提供的核心線程(Kernel-Level Thread ,KLT)來實作多線程。在此模型下,線程的切換排程由系統核心完成,系統核心負責将多個線程執行的任務映射到各個CPU中去執行。

    程式一般不會直接去使用核心線程,而是去使用核心線程的一種進階接口——輕量級程序(Light Weight Process,LWP),輕量級程序就是我們通常意義上所講的線程,由于每個輕量級程序都由一個核心線程支援,是以隻有先支援核心線程,才能有輕量級程序。這種輕量級程序與核心線程之間1:1的關系稱為一對一的線程模型。

一文帶你讀懂:系統線程模型與實作原理
  • 使用者線程模型:

    從廣義上來講,一個線程隻要不是核心線程,就可以認為是使用者線程(User Thread,UT),是以,從這個定義上來講,輕量級程序也屬于使用者線程,但輕量級程序的實作始終是建立在核心之上的,許多操作都要進行系統調用,效率會受到限制。

    使用使用者線程的優勢在于不需要系統核心支援,劣勢也在于沒有系統核心的支援,所有的線程操作都需要使用者程式自己處理。線程的建立、切換和排程都是需要考慮的問題,而且由于作業系統隻把處理器資源配置設定到程序,那諸如“阻塞如何處理”、“多處理器系統中如何将線程映射到其他處理器上”這類問題解決起來将會異常困難,甚至不可能完成。

    因而使用使用者線程實作的程式一般都比較複雜,此處所講的“複雜”與“程式自己完成線程操作”,并不限制程式中必須編寫了複雜的實作使用者線程的代碼,使用使用者線程的程式,很多都依賴特定的線程庫來完成基本的線程操作,這些複雜性都封裝線上程庫之中,除了以前在不支援多線程的作業系統中(如DOS)的多線程程式與少數有特殊需求的程式外,現在使用使用者線程的程式越來越少了,Java、Ruby等語言都曾經使用過使用者線程,最終又都放棄使用它。

一文帶你讀懂:系統線程模型與實作原理
  • 混合線程模型:

    線程除了依賴核心線程實作和完全由使用者程式自己實作之外,還有一種将核心線程與使用者線程一起使用的實作方式。在這種混合實作下,既存在使用者線程,也存在輕量級程序。

    使用者線程還是完全建立在使用者空間中,是以使用者線程的建立、切換、析構等操作依然廉價,并且可以支援大規模的使用者線程并發。而作業系統提供支援的輕量級程序則作為使用者線程和核心線程之間的橋梁,這樣可以使用核心提供的線程排程功能及處理器映射,并且使用者線程的系統調用要通過輕量級線程來完成,大大降低了整個程序被完全阻塞的風險。

    在這種混合模式中,使用者線程與輕量級程序的數量比是不定的,即為N:M的關系。許多UNIX系列的作業系統,如Solaris、HP-UX等都提供了N:M的線程模型實作。

    對于Sun JDK來說,它的Windows版與Linux版都是使用一對一的線程模型實作的,一條Java線程就映射到一條輕量級程序之中,因為Windows和Linux系統提供的線程模型就是一對一的。在Solaris平台中,由于作業系統的線程特性可以同時支援一對一(通過Bound Threads或Alternate Libthread實作)及多對多(通過LWP/Thread Based Synchronization實作)的線程模型,是以在Solaris版的JDK中也對應提供了兩個平台專有的虛拟機參數:-XX:+UseLWPSynchronization(預設值)和-XX:+UseBoundThreads來明确指定虛拟機使用哪種線程模型。

一文帶你讀懂:系統線程模型與實作原理
一文帶你讀懂:系統線程模型與實作原理

作業系統的線程排程方式

線程排程是指系統為線程配置設定處理器使用權的過程。

主要的線程排程方式有兩種,分别是 協同式線程排程(Cooperative Threads-Scheduling)和 搶占式線程排程(Preemptive Threads-Scheduling),見下圖。

一文帶你讀懂:系統線程模型與實作原理

協同式排程如果使用協同式排程的多線程系統,線程的執行時間由線程本身來控制,線程把自己的工作執行完了之後,要主動通知系統切換到另外一個線程上。協同式多線程的最大好處是實作簡單,而且由于線程要把自己的事情幹完後才會進行線程切換,切換操作對線程自己是可知的,是以沒有什麼線程同步的問題。Lua語言中的“協同例程”就是這類實作。它的壞處也很明顯:線程執行時間不可控制,甚至如果一個線程編寫有問題,一直不告知系統進行線程切換,那麼程式就會一直阻塞在那裡。很久以前的Windows 3.x系統就是使用協同式來實作多程序多任務的,相當不穩定,一個程序堅持不讓出CPU執行時間就可能會導緻整個系統崩潰。

搶占式排程

如果使用搶占式排程的多線程系統,那麼每個線程将由系統來配置設定執行時間,線程的切換不由線程本身來決定(在Java中,Thread.yield()可以讓出執行時間,但是要擷取執行時間的話,線程本身是沒有什麼辦法的)。

在這種實作線程排程的方式下,線程的執行時間是系統可控的,也不會有一個線程導緻整個程序阻塞的問題。

Java使用的線程排程方式就是搶占式排程。在JDK後續版本中有可能會提供協程(Coroutines)方式來進行多任務處理。

與前面所說的Windows 3.x的例子相對,在Windows 9x/NT核心中就是使用搶占式來實作多程序的,當一個程序出了問題,我們還可以使用任務管理器把這個程序“殺掉”,而不至于導緻系統崩潰。

線程優先級

雖然Java線程排程是系統自動完成的,但是我們還是可以“建議”系統給某些線程多配置設定一點執行時間,另外的一些線程則可以少配置設定一點——這項操作可以通過設定線程優先級來完成。

Java語言一共設定了10個級别的線程優先級(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),在兩個線程同時處于Ready狀态時,優先級越高的線程越容易被系統選擇執行。

不過,線程優先級并不是太靠譜,原因是Java的線程是通過映射到系統的原生線程上來實作的,是以線程排程最終還是取決于作業系統,雖然現在很多作業系統都提供線程優先級的概念,但是并不見得能與Java線程的優先級一一對應。

如Solaris中有2147483648(232)種優先級,但Windows中就隻有7種,比Java線程優先級多的系統還好說,中間留下一點空位就可以了,但比Java線程優先級少的系統,就不得不出現幾個優先級相同的情況了。

一文帶你讀懂:系統線程模型與實作原理

上圖顯示了Java線程優先級與Windows線程優先級之間的對應關系,Windows平台的JDK中使用了除THREAD_PRIORITY_IDLE之外的其餘6種線程優先級。

一文帶你讀懂:系統線程模型與實作原理

Java 線程狀态

一文帶你讀懂:系統線程模型與實作原理

Java語言定義了5種線程狀态,在任意一個時間點,一個線程隻能有且隻有其中的一種狀态,這5種狀态分别如下:

一文帶你讀懂:系統線程模型與實作原理
  1. 建立(New):建立後尚未啟動的線程處于這種狀态。
  2. 運作(Runable):Runable包括了作業系統線程狀态中的Running和Ready,也就是處于此狀态的線程有可能正在執行,也有可能正在等待着CPU為它配置設定執行時間。
  3. 無限期等待(Waiting):處于這種狀态的線程不會被配置設定CPU執行時間,它們要等待被其他線程顯式地喚醒。
    以下方法會讓線程陷入無限期的等待狀态:              沒有設定Timeout參數的Object.wait()方法。              沒有設定Timeout參數的Thread.join()方法。              LockSupport.park()方法。           
  4. 限期等待(Timed Waiting):處于這種狀态的線程也不會被配置設定CPU執行時間,不過無須等待被其他線程顯式地喚醒,在一定時間之後它們會由系統自動喚醒。
    以下方法會讓線程進入限期等待狀态:              Thread.sleep()方法。              設定了Timeout參數的Object.wait()方法。              設定了Timeout參數的Thread.join()方法。              LockSupport.parkNanos()方法。              LockSupport.parkUntil()方法。           
  5. 阻塞(Blocked):線程被阻塞了,“阻塞狀态”與“等待狀态”的差別是:“阻塞狀态”在等待着擷取到一個排他鎖,這個事件将在另外一個線程放棄這個鎖的時候發生;而“等待狀态”則是在等待一段時間,或者喚醒動作的發生。在程式等待進入同步區域的時候,線程将進入這種狀态。
  6. 結束(Terminated):已終止線程的線程狀态,線程已經結束執行。
一文帶你讀懂:系統線程模型與實作原理

Java 多線程實作

實作1:繼承Thread類

// 繼承 Thread              public class MyThread extends Thread {              @Override              public void run() {              System.out.println("MyThread run...");              }              }           

實作2:實作 Runnable 接口

public class MyRunnable implements Runnable {              @Override              public void run() {              System.out.println("MyRunnable run...");              }              }           

實作3:實作 Callable 接口,使用 FutureTask 擷取異步傳回值

public static void main(String[] args) throws ExecutionException, InterruptedException {              class MyCallable implements Callable<String> {              @Override              public String call() throws Exception {              return "MyCallable";              }              }              FutureTask<String> task = new FutureTask<>(new MyCallable());              Thread c = new Thread(task);              c.start();              System.out.println(task.get());              }           

實作4:JDK8以上版本使用 CompletableFuture 進行異步計算。

在Java8中,提供了非常強大的Future的擴充功能,可以幫助我們簡化異步程式設計的複雜性,并且提供了函數式程式設計的能力,可以通過回調的方式處理計算結果,也提供了轉換群組合 CompletableFuture 的方法。

public class CompletableFutureTest {              public static void main(String[] args) {              ExecutorService threadPool = Executors.newFixedThreadPool(2);              // JDK1.8 提供的 CompletableFuture              CompletableFuture<String> futureTask = CompletableFuture.supplyAsync(new Supplier<String>() {              @Override              public String get() {              System.out.println("task start");              try {              Thread.sleep(10000);              } catch (Exception e) {              e.printStackTrace();              return "execute failure";              }              System.out.println("task end");              return "execute success";              }              }, threadPool);              // 異步擷取 futureTask 的執行結果,此處代碼可以跟其他流程代碼放在一起              futureTask.thenAccept(e-> System.out.println("future task result:" + e));              System.out.println("main thread end");              }              }                  輸出結果:              task start              main thread end              task end              future task result:execute success           

實作5:使用線程池,

ThreadPoolExecutor 類

public ThreadPoolExecutor(int corePoolSize,              int maximumPoolSize,              long keepAliveTime,              TimeUnit unit,              BlockingQueue<Runnable> workQueue,              ThreadFactory threadFactory) {              this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler);              }              <T> Future<T> submit(Callable<T> task);              Future<?> submit(Runnable task);           
一文帶你讀懂:系統線程模型與實作原理

總結

本節主要講述了作業系統提供的三種線程模型和兩種線程排程方式,同時補充了基于Java的5種多線程實作和6個線程狀态的相關知識。

希望各位回顧知識的同時也有新收獲。GoodLuck!!

一文帶你讀懂:系統線程模型與實作原理

—END—

掃描二維碼

擷取技術幹貨

一文帶你讀懂:系統線程模型與實作原理