1. 程式人生 > >深入分析 Java、Kotlin、Go 的執行緒和協程

深入分析 Java、Kotlin、Go 的執行緒和協程

![](https://img2020.cnblogs.com/other/633265/202012/633265-20201211165836556-115473640.jpg) - [前言](#前言) - [協程是什麼](#協程是什麼) - [協程的好處](#協程的好處) - [程序](#程序) - [程序是什麼](#程序是什麼) - [程序組成](#程序組成) - [程序特徵](#程序特徵) - [執行緒](#執行緒) - [執行緒是什麼](#執行緒是什麼) - [執行緒組成](#執行緒組成) - [任務排程](#任務排程) - [程序與執行緒的區別](#程序與執行緒的區別) - [執行緒的實現模型](#執行緒的實現模型) - [一對一模型](#一對一模型) - [多對一模型](#多對一模型) - [多對多模型](#多對多模型) - [執行緒的“併發”](#執行緒的併發) - [協程](#協程) - [協程的目的](#協程的目的) - [協程的特點](#協程的特點) - [協程的原理](#協程的原理) - [Java、Kotlin、Go 的執行緒與協程](#javakotlingo-的執行緒與協程) - [Kotlin 的協程](#kotlin-的協程) - [使用「執行緒」的程式碼](#使用執行緒的程式碼) - [使用「協程」的程式碼](#使用協程的程式碼) - [Go 的協程](#go-的協程) - [Java 的 Kilim 協程框架](#java-的-kilim-協程框架) - [Java 的 Project Loom](#java-的-project-loom) - [使用 Fiber](#使用-fiber) - [總結](#總結) - [參考資料](#參考資料) # 前言 Go 語言比 Java 語言效能優越的一個原因,就是輕量級執行緒`Goroutines`(協程Coroutine)。本篇文章深入分析下 Java 的執行緒和 Go 的協程。 ## 協程是什麼 協程並不是 Go 提出來的新概念,其他的一些程式語言,例如:Go、Python 等都可以在語言層面上實現協程,甚至是 Java,也可以通過使用擴充套件庫來間接地支援協程。 當在網上搜索協程時,我們會看到: - Kotlin 官方文件說「本質上,協程是輕量級的執行緒」。 - 很多部落格提到「不需要從使用者態切換到核心態」、「是協作式的」等等。 「協程 Coroutines」源自 Simula 和 Modula-2 語言,這個術語早在 1958 年就被 Melvin Edward Conway 發明並用於構建彙編程式,說明協程是一種程式設計思想,並不侷限於特定的語言。 ## 協程的好處 效能比 Java 好很多,甚至程式碼實現都比 Java 要簡潔很多。 那這究竟又是為什麼呢?下面一一分析。 說明:下面關於程序和執行緒的部分,幾乎完全參考自:https://www.cnblogs.com/Survivalist/p/11527949.html,這篇文章寫得太好了~~~ # 程序 ## 程序是什麼 計算機的核心是 CPU,執行所有的計算任務;作業系統負責任務的排程、資源的分配和管理;應用程式是具有某種功能的程式,程式是執行在作業系統上的。 程序是一個具有一定獨立功能的程式在一個數據集上的一次動態執行的過程,是作業系統進行資源分配和排程的一個獨立單位,是應用程式執行的載體。 ## 程序組成 程序由三部分組成: - `程式`:描述程序要完成的功能,是控制程序執行的指令集。 - `資料集合`:程式在執行時所需要的資料和工作區。 - `程序控制塊`:(Program Control Block,簡稱PCB),包含程序的描述資訊和控制資訊,是程序存在的唯一標誌。 ## 程序特徵 - 動態性:程序是程式的一次執行過程,是臨時的,有生命期的,是動態產生,動態消亡的。 - 併發性:任何程序都可以同其他程序一起併發執行。 - 獨立性:程序是系統進行資源分配和排程的一個獨立單位。 - 結構性:程序由程式、資料和程序控制塊三部分組成。 # 執行緒 ## 執行緒是什麼 執行緒是程式執行中一個單一的`順序控制流程`,是`程式執行流的最小單元`,是`處理器排程和分派的基本單位`。一個程序可以有一個或多個執行緒,各個執行緒之間`共享程式的記憶體空間`(也就是所在程序的記憶體空間)。 ## 執行緒組成 - 執行緒ID、當前指令指標(PC) - 暫存器 - 堆疊 ## 任務排程 大部分作業系統(如Windows、Linux)的任務排程是採用`時間片輪轉的搶佔式排程方式`。 在一個程序中,當一個執行緒任務執行幾毫秒後,會由作業系統的核心(負責管理各個任務)進行排程,通過硬體的計數器中斷處理器,讓該執行緒強制暫停並將該執行緒的暫存器放入記憶體中,通過檢視執行緒列表決定接下來執行哪一個執行緒,並從記憶體中恢復該執行緒的暫存器,最後恢復該執行緒的執行,從而去執行下一個任務。 ## 程序與執行緒的區別 - 執行緒是程式執行的最小單位,而程序是作業系統分配資源的最小單位; - 一個程序由一個或多個執行緒組成,`執行緒是一個程序中程式碼的不同執行路線`; - 程序之間相互獨立,但同一程序下的各個執行緒之間共享程式的記憶體空間(包括程式碼段、資料集、堆等)及一些程序級的資源(如開啟檔案和訊號),某程序內的執行緒在其它程序不可見; - 排程和切換:`執行緒上下文切換`比`程序上下文切換`要`快`得多。 ![](https://img2020.cnblogs.com/other/633265/202012/633265-20201211165836816-1113514597.jpg) ## 執行緒的實現模型 程式一般不會直接去使用核心執行緒,而是去使用核心執行緒的一種高階介面——`輕量級程序(Lightweight Process,LWP)`,輕量級程序就是我們通常意義上所講的執行緒,也被叫做使用者執行緒。 ### 一對一模型 一個使用者執行緒對應一個核心執行緒,如果是多核的 CPU,那麼執行緒之間是真正的併發。 缺點: - 核心執行緒的數量有限,一對一模型使用的使用者執行緒數量有限制。 - 核心執行緒的排程,上下文切換的開銷較大(雖然沒有程序上下文切換的開銷大),導致使用者執行緒的執行效率下降。 ### 多對一模型 `多個使用者執行緒`對映到`一個核心執行緒`上,執行緒間的切換由`使用者態`的程式碼來進行。使用者執行緒的建立、同步、銷燬都是在使用者態中完成,不需要核心的介入。因此多對一的上下文切換速度快很多,且使用者執行緒的數量幾乎沒有限制。 缺點: - 若一個使用者執行緒阻塞,其他所有執行緒都無法執行,此時核心執行緒處於阻塞狀態。 - 處理器數量的增加,不會對多對一模型的執行緒效能造成影響,因為所有的使用者執行緒都對映到了一個處理器上。 ### 多對多模型 結合了`一對一模型`和`多對一`模型的優點,多個使用者執行緒對映到多個核心執行緒上,由`執行緒庫`負責在可用的可排程實體上排程使用者執行緒。這樣執行緒間的上下文切換很快,因為它避免了系統呼叫。但是增加了系統的複雜性。 優點: - 一個使用者執行緒的阻塞不會導致所有執行緒的阻塞,因為此時還有別的核心執行緒被排程來執行; - 多對多模型對使用者執行緒的數量沒有限制; - 在多處理器的作業系統中,多對多模型的執行緒也能得到一定的效能提升,但提升的幅度不如一對一模型的高。 ## 執行緒的“併發” 只有在執行緒的數量 < 處理器的數量時,執行緒的併發才是真正的併發,這時不同的執行緒執行在不同的處理器上。但是當執行緒的數量 > 處理器的數量時,會出現一個處理器執行多個執行緒的情況。 在單個處理器執行多個執行緒時,併發是一種模擬出來的狀態。作業系統採用時間片輪轉的方式輪流執行每一個執行緒。現在,幾乎所有的現代作業系統採用的都是時間片輪轉的搶佔式排程方式。 # 協程 當在網上搜索協程時,我們會看到: - 本質上,協程是輕量級的執行緒。 - 很多部落格提到「不需要從使用者態切換到核心態」、「是協作式的」。 協程也並不是 Go 提出來的,協程是一種程式設計思想,並不侷限於特定的語言。Go、Python、Kotlin 都可以在語言層面上實現協程,Java 也可以通過擴充套件庫的方式間接支援協程。 協程比執行緒更加輕量級,可以由程式設計師自己管理的輕量級執行緒,對核心不可見。 ## 協程的目的 在傳統的 J2EE 系統中都是基於每個請求佔用一個執行緒去完成完整的業務邏輯(包括事務)。所以系統的吞吐能力取決於每個執行緒的操作耗時。如果遇到很耗時的 I/O 行為,則整個系統的吞吐立刻下降,因為這個時候執行緒一直處於阻塞狀態,如果執行緒很多的時候,會存在很多執行緒處於空閒狀態(等待該執行緒執行完才能執行),造成了資源應用不徹底。 最常見的例子就是 JDBC(它是同步阻塞的),這也是為什麼很多人都說資料庫是瓶頸的原因。這裡的耗時其實是讓 CPU 一直在等待 I/O 返回,說白了執行緒根本沒有利用 CPU 去做運算,而是處於空轉狀態。而另外過多的執行緒,也會帶來更多的 ContextSwitch 開銷。 對於上述問題,現階段行業裡的比較流行的解決方案之一就是單執行緒加上非同步回撥。其代表派是 node.js 以及 Java 裡的新秀 Vert.x。 而協程的目的就是當出現長時間的 I/O 操作時,通過讓出目前的協程排程,執行下一個任務的方式,來消除 ContextSwitch 上的開銷。 ## 協程的特點 - 執行緒的切換由作業系統負責排程,協程由使用者自己進行排程,減少了上下文切換,提高了效率 - 執行緒的預設 Stack 是1M,協程更加輕量,是 1K,在相同記憶體中可以開啟更多的協程。 - 由於在同一個執行緒上,因此可以`避免競爭關係`而使用鎖。 - 適用於`被阻塞的`,且需要大量併發的場景。但不適用於大量計算的多執行緒,遇到此種情況,更好用執行緒去解決。 ## 協程的原理 當出現IO阻塞的時候,由協程的排程器進行排程,通過將資料流立刻yield掉(主動讓出),並且記錄當前棧上的資料,阻塞完後立刻再通過執行緒恢復棧,並把阻塞的結果放到這個執行緒上去跑,這樣看上去好像跟寫同步程式碼沒有任何差別,這整個流程可以稱為`coroutine`,而跑在由coroutine負責排程的執行緒稱為`Fiber`。比如Golang裡的 go關鍵字其實就是負責開啟一個`Fiber`,讓func邏輯跑在上面。 由於協程的暫停完全由程式控制,發生在使用者態上;而執行緒的阻塞狀態是由作業系統核心來進行切換,發生在核心態上。 因此,協程的開銷遠遠小於執行緒的開銷,也就沒有了 ContextSwitch 上的開銷。 假設程式中預設建立兩個執行緒為協程使用,在主執行緒中建立協程ABCD…,分別儲存在就緒佇列中,排程器首先會分配一個工作執行緒A執行協程A,另外一個工作執行緒B執行協程B,其它建立的協程將會放在佇列中進行排隊等待。 ![](https://img2020.cnblogs.com/other/633265/202012/633265-20201211165837001-1582226810.jpg) 當協程A呼叫暫停方法或被阻塞時,協程A會進入到掛起佇列,排程器會呼叫等待佇列中的其它協程搶佔執行緒A執行。當協程A被喚醒時,它需要重新進入到就緒佇列中,通過排程器搶佔執行緒,如果搶佔成功,就繼續執行協程A,失敗則繼續等待搶佔執行緒。 ![](https://img2020.cnblogs.com/other/633265/202012/633265-20201211165837317-966432289.jpg) # Java、Kotlin、Go 的執行緒與協程 Java 在 Linux 作業系統下使用的是使用者執行緒+輕量級執行緒,`一個使用者執行緒對映到一個核心執行緒`,執行緒之間的切換就涉及到了上下文切換。所以在 Java 中並不適合建立大量的執行緒,否則效率會很低。可以先看下 Kotlin 和 Go 的協程: ## Kotlin 的協程 Kotlin 在誕生之初,目標就是完全相容 Java,卻是一門非常務實的語言,其中一個特性,就是支援協程。 但是 Kotlin 最終還是執行在 JVM 中的,目前的 JVM 並不支援協程,Kotlin 作為一門程式語言,也只是能在語言層面支援協程。Kotlin 的協程是用於非同步程式設計等場景的,在語言級提供協程支援,而將大部分功能委託給庫。 ### 使用「執行緒」的程式碼 ```java @Test fun testThread() { // 執行時間 1min+ val c = AtomicLong() for (i in 1..1_000_000L) thread(start = true) { c.addAndGet(i) } println(c.get()) } ``` 上述程式碼建立了 `100 萬個執行緒`,在每個執行緒裡僅僅呼叫了 add 操作,但是由於建立執行緒太多,這個測試用例在我的機器上要跑 1 分鐘左右。 ### 使用「協程」的程式碼 ```java @Test fun testLaunch() { val c = AtomicLong() runBlocking { for (i in 1..1_000_000L) launch { c.addAndGet(workload(i)) } } print(c.get()) } suspend fun workload(n: Long): Long { delay(1000) return n } ``` 這段程式碼是建立了 `100 萬個協程`,測試用例在我的機器上執行時間大概是 10 秒鐘。而且這段程式碼的每個協程都 delay 了 1 秒鐘,執行效率仍然遠遠高於執行緒。 詳細的語法可以檢視 Kotlin 的官方網站:https://www.kotlincn.net/docs/reference/coroutines/basics.html 其中關鍵字 `launch` 是開啟了一個協程,關鍵字 `suspend` 是掛起一個協程,而不會阻塞。現在在看這個流程,應該就懂了~ ![](https://img2020.cnblogs.com/other/633265/202012/633265-20201211165837567-1268128701.jpg) ## Go 的協程 官方例程:https://gobyexample-cn.github.io/goroutines go語言層面並`不支援多程序或多執行緒`,但是協程更好用,協程被稱為使用者態執行緒,不存在CPU上下文切換問題,效率非常高。下面是一個簡單的協程演示程式碼: ```go package main func main() { go say("Hello World") } func say(s string) { println(s) } ``` ## Java 的 Kilim 協程框架 目前 Java 原生語言暫時不支援協程,可以使用 [kilim](https://github.com/kilim/kilim),具體原理可以看官方文件,暫時還沒有研究~ ## Java 的 Project Loom Java 也在逐步支援協程,其專案就是 `Project Loom`(https://openjdk.java.net/projects/loom/)。這個專案在18年底的時候已經達到可初步演示的原型階段。不同於之前的方案,Project Loom 是從 JVM 層面對多執行緒技術進行徹底的改變。 官方介紹: http://cr.openjdk.java.net/~rpressler/loom/Loom-Proposal.html 其中一段介紹了為什麼引入這個專案: One of Java's most important contributions when it was first released, over twenty years ago, was the easy access to threads and synchronization primitives. Java threads (either used directly, or indirectly through, for example, Java servlets processing HTTP requests) provided a relatively simple abstraction for writing concurrent applications. These days, however, one of the main difficulties in writing concurrent programs that meet today's requirements is that the software unit of concurrency offered by the runtime — the thread — cannot match the scale of the domain's unit of concurrency, be it a user, a transaction or even a single operation. Even if the unit of application concurrency is coarse — say, a session, represented by single socket connection — a server can handle upward of a million concurrent open sockets, yet the Java runtime, which uses the operating system's threads for its implementation of Java threads, cannot efficiently handle more than a few thousand. A mismatch in several orders of magnitude has a big impact. 文章大意就是本文上面所說的,Java 的使用者執行緒與核心執行緒是一對一的關係,一個 Java 程序很難建立上千個執行緒,如果是對於 I/O 阻塞的程式(例如資料庫讀取/Web服務),效能會很低下,所以要採用類似於協程的機制。 ### 使用 Fiber 在引入 Project Loom 之後,JDK 將引入一個新類:java.lang.Fiber。此類與 java.lang.Thread 一起,都成為了 java.lang.Strand 的子類。即執行緒變成了一個虛擬的概念,有兩種實現方法:Fiber 所表示的輕量執行緒和 Thread 所表示的傳統的重量級執行緒。 ```java Fiber f = Fiber.schedule(() -> { println("Hello 1"); lock.lock(); // 等待鎖不會掛起執行緒 try { println("Hello 2"); } finally { lock.unlock(); } println("Hello 3"); }) ``` 只需執行 `Fiber.schedule(Runnable task)` 就能在 `Fiber` 中執行任務。最重要的是,上面例子中的 lock.lock() 操作將不再掛起底層執行緒。除了 `Lock 不再掛起執行緒`以外,像 `Socket BIO 操作也不再掛起執行緒`。 但 synchronized,以及 Native 方法中執行緒掛起操作無法避免。 # 總結 協程大法好,比執行緒更輕量級,但是僅針對 I/O 阻塞才有效;對於 CPU 密集型的應用,因為 CPU 一直都在計算並沒有什麼空閒,所以沒有什麼作用。 Kotlin 相容 Java,在編譯器、語言層面實現了協程,JVM 底層並不支援協程;Go 天生就是支援協程的,不支援多程序和多執行緒。Java 的 `Project Loom` 專案支援協程, # 參考資料 - 極客時間-Java效能調優實戰/19.如何用協程來優化多執行緒業務? - https://www.cnblogs.com/Survivalist/p/11527949.html - https://www.jianshu.com/p/5db701a764cb # 公眾號 coding 筆記、點滴記錄,以後的文章也會同步到公眾號(Coding Insight)中,希望大家關注^_^ 程式碼和思維導圖在 [GitHub 專案](https://github.com/LjyYano/Thinking_in_Java_MindMapping)中,歡迎大家 star! ![](https://img2020.cnblogs.com/other/633265/202012/633265-20201211165837764-1451045116.jpg)