深入理解 Java、Kotlin、Go 的線程和協(xié)程
前言
Go 語(yǔ)言比 Java 語(yǔ)言性能優(yōu)越的一個(gè)原因,就是輕量級(jí)線程Goroutines
(協(xié)程Coroutine)。本篇文章深入分析下 Java 的線程和 Go 的協(xié)程。
協(xié)程是什么
協(xié)程并不是 Go 提出來(lái)的新概念,其他的一些編程語(yǔ)言,例如:Go、Python 等都可以在語(yǔ)言層面上實(shí)現(xiàn)協(xié)程,甚至是 Java,也可以通過(guò)使用擴(kuò)展庫(kù)來(lái)間接地支持協(xié)程。
當(dāng)在網(wǎng)上搜索協(xié)程時(shí),我們會(huì)看到:
- Kotlin 官方文檔說(shuō)「本質(zhì)上,協(xié)程是輕量級(jí)的線程」。
- 很多博客提到「不需要從用戶態(tài)切換到內(nèi)核態(tài)」、「是協(xié)作式的」等等。
「協(xié)程 Coroutines」源自 Simula 和 Modula-2 語(yǔ)言,這個(gè)術(shù)語(yǔ)早在 1958 年就被 Melvin Edward Conway 發(fā)明并用于構(gòu)建匯編程序,說(shuō)明協(xié)程是一種編程思想,并不局限于特定的語(yǔ)言。
協(xié)程的好處
性能比 Java 好很多,甚至代碼實(shí)現(xiàn)都比 Java 要簡(jiǎn)潔很多。
那這究竟又是為什么呢?下面一一分析。
說(shuō)明:下面關(guān)于進(jìn)程和線程的部分,幾乎完全參考自:http://www.dbjr.com.cn/article/114368.htm
進(jìn)程
進(jìn)程是什么
計(jì)算機(jī)的核心是 CPU,執(zhí)行所有的計(jì)算任務(wù);操作系統(tǒng)負(fù)責(zé)任務(wù)的調(diào)度、資源的分配和管理;應(yīng)用程序是具有某種功能的程序,程序是運(yùn)行在操作系統(tǒng)上的。
進(jìn)程是一個(gè)具有一定獨(dú)立功能的程序在一個(gè)數(shù)據(jù)集上的一次動(dòng)態(tài)執(zhí)行的過(guò)程,是操作系統(tǒng)進(jìn)行資源分配和調(diào)度的一個(gè)獨(dú)立單位,是應(yīng)用程序運(yùn)行的載體。
進(jìn)程組成
進(jìn)程由三部分組成:
程序
:描述進(jìn)程要完成的功能,是控制進(jìn)程執(zhí)行的指令集。數(shù)據(jù)集合
:程序在執(zhí)行時(shí)所需要的數(shù)據(jù)和工作區(qū)。進(jìn)程控制塊
:(Program Control Block,簡(jiǎn)稱PCB),包含進(jìn)程的描述信息和控制信息,是進(jìn)程存在的唯一標(biāo)志。
進(jìn)程特征
- 動(dòng)態(tài)性:進(jìn)程是程序的一次執(zhí)行過(guò)程,是臨時(shí)的,有生命期的,是動(dòng)態(tài)產(chǎn)生,動(dòng)態(tài)消亡的。
- 并發(fā)性:任何進(jìn)程都可以同其他進(jìn)程一起并發(fā)執(zhí)行。
- 獨(dú)立性:進(jìn)程是系統(tǒng)進(jìn)行資源分配和調(diào)度的一個(gè)獨(dú)立單位。結(jié)構(gòu)性:進(jìn)程由程序、數(shù)據(jù)和進(jìn)程控制塊三部分組成。
線程
線程是什么
線程是程序執(zhí)行中一個(gè)單一的順序控制流程
,是程序執(zhí)行流的最小單元
,是處理器調(diào)度和分派的基本單位
。一個(gè)進(jìn)程可以有一個(gè)或多個(gè)線程,各個(gè)線程之間共享程序的內(nèi)存空間
(也就是所在進(jìn)程的內(nèi)存空間)。
線程組成線程ID、當(dāng)前指令指針(PC)寄存器堆棧
任務(wù)調(diào)度
大部分操作系統(tǒng)(如Windows、Linux)的任務(wù)調(diào)度是采用時(shí)間片輪轉(zhuǎn)的搶占式調(diào)度方式
。
在一個(gè)進(jìn)程中,當(dāng)一個(gè)線程任務(wù)執(zhí)行幾毫秒后,會(huì)由操作系統(tǒng)的內(nèi)核(負(fù)責(zé)管理各個(gè)任務(wù))進(jìn)行調(diào)度,通過(guò)硬件的計(jì)數(shù)器中斷處理器,讓該線程強(qiáng)制暫停并將該線程的寄存器放入內(nèi)存中,通過(guò)查看線程列表決定接下來(lái)執(zhí)行哪一個(gè)線程,并從內(nèi)存中恢復(fù)該線程的寄存器,最后恢復(fù)該線程的執(zhí)行,從而去執(zhí)行下一個(gè)任務(wù)。
進(jìn)程與線程的區(qū)別
線程是程序執(zhí)行的最小單位,而進(jìn)程是操作系統(tǒng)分配資源的最小單位;一個(gè)進(jìn)程由一個(gè)或多個(gè)線程組成,線程是一個(gè)進(jìn)程中代碼的不同執(zhí)行路線
;進(jìn)程之間相互獨(dú)立,但同一進(jìn)程下的各個(gè)線程之間共享程序的內(nèi)存空間(包括代碼段、數(shù)據(jù)集、堆等)及一些進(jìn)程級(jí)的資源(如打開文件和信號(hào)),某進(jìn)程內(nèi)的線程在其它進(jìn)程不可見(jiàn);調(diào)度和切換:線程上下文切換
比進(jìn)程上下文切換
要快
得多。
線程的實(shí)現(xiàn)模型
程序一般不會(huì)直接去使用內(nèi)核線程,而是去使用內(nèi)核線程的一種高級(jí)接口——輕量級(jí)進(jìn)程(Lightweight Process,LWP)
,輕量級(jí)進(jìn)程就是我們通常意義上所講的線程,也被叫做用戶線程。
一對(duì)一模型
一個(gè)用戶線程對(duì)應(yīng)一個(gè)內(nèi)核線程,如果是多核的 CPU,那么線程之間是真正的并發(fā)。
缺點(diǎn):
- 內(nèi)核線程的數(shù)量有限,一對(duì)一模型使用的用戶線程數(shù)量有限制。
- 內(nèi)核線程的調(diào)度,上下文切換的開銷較大(雖然沒(méi)有進(jìn)程上下文切換的開銷大),導(dǎo)致用戶線程的執(zhí)行效率下降。
多對(duì)一模型
多個(gè)用戶線程
映射到一個(gè)內(nèi)核線程
上,線程間的切換由用戶態(tài)
的代碼來(lái)進(jìn)行。用戶線程的建立、同步、銷毀都是在用戶態(tài)中完成,不需要內(nèi)核的介入。因此多對(duì)一的上下文切換速度快很多,且用戶線程的數(shù)量幾乎沒(méi)有限制。
缺點(diǎn):
- 若一個(gè)用戶線程阻塞,其他所有線程都無(wú)法執(zhí)行,此時(shí)內(nèi)核線程處于阻塞狀態(tài)。
- 處理器數(shù)量的增加,不會(huì)對(duì)多對(duì)一模型的線程性能造成影響,因?yàn)樗械挠脩艟€程都映射到了一個(gè)處理器上。
多對(duì)多模型
結(jié)合了一對(duì)一模型
和多對(duì)一
模型的優(yōu)點(diǎn),多個(gè)用戶線程映射到多個(gè)內(nèi)核線程上,由線程庫(kù)
負(fù)責(zé)在可用的可調(diào)度實(shí)體上調(diào)度用戶線程。這樣線程間的上下文切換很快,因?yàn)樗苊饬讼到y(tǒng)調(diào)用。但是增加了系統(tǒng)的復(fù)雜性。
優(yōu)點(diǎn):
一個(gè)用戶線程的阻塞不會(huì)導(dǎo)致所有線程的阻塞,因?yàn)榇藭r(shí)還有別的內(nèi)核線程被調(diào)度來(lái)執(zhí)行;多對(duì)多模型對(duì)用戶線程的數(shù)量沒(méi)有限制;在多處理器的操作系統(tǒng)中,多對(duì)多模型的線程也能得到一定的性能提升,但提升的幅度不如一對(duì)一模型的高。
線程的“并發(fā)”
只有在線程的數(shù)量 < 處理器的數(shù)量時(shí),線程的并發(fā)才是真正的并發(fā),這時(shí)不同的線程運(yùn)行在不同的處理器上。但是當(dāng)線程的數(shù)量 > 處理器的數(shù)量時(shí),會(huì)出現(xiàn)一個(gè)處理器運(yùn)行多個(gè)線程的情況。
在單個(gè)處理器運(yùn)行多個(gè)線程時(shí),并發(fā)是一種模擬出來(lái)的狀態(tài)。操作系統(tǒng)采用時(shí)間片輪轉(zhuǎn)的方式輪流執(zhí)行每一個(gè)線程?,F(xiàn)在,幾乎所有的現(xiàn)代操作系統(tǒng)采用的都是時(shí)間片輪轉(zhuǎn)的搶占式調(diào)度方式。
協(xié)程
當(dāng)在網(wǎng)上搜索協(xié)程時(shí),我們會(huì)看到:
本質(zhì)上,協(xié)程是輕量級(jí)的線程。很多博客提到「不需要從用戶態(tài)切換到內(nèi)核態(tài)」、「是協(xié)作式的」。
協(xié)程也并不是 Go 提出來(lái)的,協(xié)程是一種編程思想,并不局限于特定的語(yǔ)言。Go、Python、Kotlin 都可以在語(yǔ)言層面上實(shí)現(xiàn)協(xié)程,Java 也可以通過(guò)擴(kuò)展庫(kù)的方式間接支持協(xié)程。
協(xié)程比線程更加輕量級(jí),可以由程序員自己管理的輕量級(jí)線程,對(duì)內(nèi)核不可見(jiàn)。
協(xié)程的目的
在傳統(tǒng)的 J2EE 系統(tǒng)中都是基于每個(gè)請(qǐng)求占用一個(gè)線程去完成完整的業(yè)務(wù)邏輯(包括事務(wù))。所以系統(tǒng)的吞吐能力取決于每個(gè)線程的操作耗時(shí)。如果遇到很耗時(shí)的 I/O 行為,則整個(gè)系統(tǒng)的吞吐立刻下降,因?yàn)檫@個(gè)時(shí)候線程一直處于阻塞狀態(tài),如果線程很多的時(shí)候,會(huì)存在很多線程處于空閑狀態(tài)(等待該線程執(zhí)行完才能執(zhí)行),造成了資源應(yīng)用不徹底。
最常見(jiàn)的例子就是 JDBC(它是同步阻塞的),這也是為什么很多人都說(shuō)數(shù)據(jù)庫(kù)是瓶頸的原因。這里的耗時(shí)其實(shí)是讓 CPU 一直在等待 I/O 返回,說(shuō)白了線程根本沒(méi)有利用 CPU 去做運(yùn)算,而是處于空轉(zhuǎn)狀態(tài)。而另外過(guò)多的線程,也會(huì)帶來(lái)更多的 ContextSwitch 開銷。
對(duì)于上述問(wèn)題,現(xiàn)階段行業(yè)里的比較流行的解決方案之一就是單線程加上異步回調(diào)。其代表派是 node.js 以及 Java 里的新秀 Vert.x。
而協(xié)程的目的就是當(dāng)出現(xiàn)長(zhǎng)時(shí)間的 I/O 操作時(shí),通過(guò)讓出目前的協(xié)程調(diào)度,執(zhí)行下一個(gè)任務(wù)的方式,來(lái)消除 ContextSwitch 上的開銷。
協(xié)程的特點(diǎn)線程的切換由操作系統(tǒng)負(fù)責(zé)調(diào)度,協(xié)程由用戶自己進(jìn)行調(diào)度,減少了上下文切換,提高了效率線程的默認(rèn) Stack 是1M,協(xié)程更加輕量,是 1K,在相同內(nèi)存中可以開啟更多的協(xié)程。由于在同一個(gè)線程上,因此可以避免競(jìng)爭(zhēng)關(guān)系
而使用鎖。適用于被阻塞的
,且需要大量并發(fā)的場(chǎng)景。但不適用于大量計(jì)算的多線程,遇到此種情況,更好用線程去解決。
協(xié)程的原理
當(dāng)出現(xiàn)IO阻塞的時(shí)候,由協(xié)程的調(diào)度器進(jìn)行調(diào)度,通過(guò)將數(shù)據(jù)流立刻yield掉(主動(dòng)讓出),并且記錄當(dāng)前棧上的數(shù)據(jù),阻塞完后立刻再通過(guò)線程恢復(fù)棧,并把阻塞的結(jié)果放到這個(gè)線程上去跑,這樣看上去好像跟寫同步代碼沒(méi)有任何差別,這整個(gè)流程可以稱為coroutine
,而跑在由coroutine負(fù)責(zé)調(diào)度的線程稱為Fiber
。比如Golang里的 go關(guān)鍵字其實(shí)就是負(fù)責(zé)開啟一個(gè)Fiber
,讓func邏輯跑在上面。
由于協(xié)程的暫停完全由程序控制,發(fā)生在用戶態(tài)上;而線程的阻塞狀態(tài)是由操作系統(tǒng)內(nèi)核來(lái)進(jìn)行切換,發(fā)生在內(nèi)核態(tài)上。
因此,協(xié)程的開銷遠(yuǎn)遠(yuǎn)小于線程的開銷,也就沒(méi)有了 ContextSwitch 上的開銷。
假設(shè)程序中默認(rèn)創(chuàng)建兩個(gè)線程為協(xié)程使用,在主線程中創(chuàng)建協(xié)程ABCD…,分別存儲(chǔ)在就緒隊(duì)列中,調(diào)度器首先會(huì)分配一個(gè)工作線程A執(zhí)行協(xié)程A,另外一個(gè)工作線程B執(zhí)行協(xié)程B,其它創(chuàng)建的協(xié)程將會(huì)放在隊(duì)列中進(jìn)行排隊(duì)等待。
當(dāng)協(xié)程A調(diào)用暫停方法或被阻塞時(shí),協(xié)程A會(huì)進(jìn)入到掛起隊(duì)列,調(diào)度器會(huì)調(diào)用等待隊(duì)列中的其它協(xié)程搶占線程A執(zhí)行。當(dāng)協(xié)程A被喚醒時(shí),它需要重新進(jìn)入到就緒隊(duì)列中,通過(guò)調(diào)度器搶占線程,如果搶占成功,就繼續(xù)執(zhí)行協(xié)程A,失敗則繼續(xù)等待搶占線程。
Java、Kotlin、Go 的線程與協(xié)程
Java 在 Linux 操作系統(tǒng)下使用的是用戶線程+輕量級(jí)線程,一個(gè)用戶線程映射到一個(gè)內(nèi)核線程
,線程之間的切換就涉及到了上下文切換。所以在 Java 中并不適合創(chuàng)建大量的線程,否則效率會(huì)很低。可以先看下 Kotlin 和 Go 的協(xié)程:
Kotlin 的協(xié)程
Kotlin 在誕生之初,目標(biāo)就是完全兼容 Java,卻是一門非常務(wù)實(shí)的語(yǔ)言,其中一個(gè)特性,就是支持協(xié)程。
但是 Kotlin 最終還是運(yùn)行在 JVM 中的,目前的 JVM 并不支持協(xié)程,Kotlin 作為一門編程語(yǔ)言,也只是能在語(yǔ)言層面支持協(xié)程。Kotlin 的協(xié)程是用于異步編程等場(chǎng)景的,在語(yǔ)言級(jí)提供協(xié)程支持,而將大部分功能委托給庫(kù)。
使用「線程」的代碼
@Test fun testThread() { // 執(zhí)行時(shí)間 1min+ val c = AtomicLong() for (i in 1..1_000_000L) thread(start = true) { c.addAndGet(i) } println(c.get()) }
上述代碼創(chuàng)建了 100 萬(wàn)個(gè)線程
,在每個(gè)線程里僅僅調(diào)用了 add 操作,但是由于創(chuàng)建線程太多,這個(gè)測(cè)試用例在我的機(jī)器上要跑 1 分鐘左右。
使用「協(xié)程」的代碼
@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
這段代碼是創(chuàng)建了 100 萬(wàn)個(gè)協(xié)程
,測(cè)試用例在我的機(jī)器上執(zhí)行時(shí)間大概是 10 秒鐘。而且這段代碼的每個(gè)協(xié)程都 delay 了 1 秒鐘,執(zhí)行效率仍然遠(yuǎn)遠(yuǎn)高于線程。
詳細(xì)的語(yǔ)法可以查看 Kotlin 的官方網(wǎng)站:https://www.kotlincn.net/docs/reference/coroutines/basics.html
其中關(guān)鍵字 launch
是開啟了一個(gè)協(xié)程,關(guān)鍵字 suspend
是掛起一個(gè)協(xié)程,而不會(huì)阻塞?,F(xiàn)在在看這個(gè)流程,應(yīng)該就懂了~
Go 的協(xié)程
官方例程:https://gobyexample-cn.github.io/goroutines
go語(yǔ)言層面并不支持多進(jìn)程或多線程
,但是協(xié)程更好用,協(xié)程被稱為用戶態(tài)線程,不存在CPU上下文切換問(wèn)題,效率非常高。下面是一個(gè)簡(jiǎn)單的協(xié)程演示代碼:
package main func main() { go say("Hello World") } func say(s string) { println(s) }
Java 的 Kilim 協(xié)程框架
目前 Java 原生語(yǔ)言暫時(shí)不支持協(xié)程,可以使用 kilim,具體原理可以看官方文檔,暫時(shí)還沒(méi)有研究~
Java 的 Project Loom
Java 也在逐步支持協(xié)程,其項(xiàng)目就是 Project Loom
(https://openjdk.java.net/projects/loom/)。這個(gè)項(xiàng)目在18年底的時(shí)候已經(jīng)達(dá)到可初步演示的原型階段。不同于之前的方案,Project Loom 是從 JVM 層面對(duì)多線程技術(shù)進(jìn)行徹底的改變。
官方介紹:
http://cr.openjdk.java.net/~rpressler/loom/Loom-Proposal.html
其中一段介紹了為什么引入這個(gè)項(xiàng)目:
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.
文章大意就是本文上面所說(shuō)的,Java 的用戶線程與內(nèi)核線程是一對(duì)一的關(guān)系,一個(gè) Java 進(jìn)程很難創(chuàng)建上千個(gè)線程,如果是對(duì)于 I/O 阻塞的程序(例如數(shù)據(jù)庫(kù)讀取/Web服務(wù)),性能會(huì)很低下,所以要采用類似于協(xié)程的機(jī)制。
使用 Fiber
在引入 Project Loom 之后,JDK 將引入一個(gè)新類:java.lang.Fiber。此類與 java.lang.Thread 一起,都成為了 java.lang.Strand 的子類。即線程變成了一個(gè)虛擬的概念,有兩種實(shí)現(xiàn)方法:Fiber 所表示的輕量線程和 Thread 所表示的傳統(tǒng)的重量級(jí)線程。
Fiber f = Fiber.schedule(() -> { println("Hello 1"); lock.lock(); // 等待鎖不會(huì)掛起線程 try { println("Hello 2"); } finally { lock.unlock(); } println("Hello 3"); })
只需執(zhí)行 Fiber.schedule(Runnable task)
就能在 Fiber
中執(zhí)行任務(wù)。最重要的是,上面例子中的 lock.lock() 操作將不再掛起底層線程。除了 Lock 不再掛起線程
以外,像 Socket BIO 操作也不再掛起線程
。 但 synchronized,以及 Native 方法中線程掛起操作無(wú)法避免。
總結(jié)
協(xié)程大法好,比線程更輕量級(jí),但是僅針對(duì) I/O 阻塞才有效;對(duì)于 CPU 密集型的應(yīng)用,因?yàn)?CPU 一直都在計(jì)算并沒(méi)有什么空閑,所以沒(méi)有什么作用。
Kotlin 兼容 Java,在編譯器、語(yǔ)言層面實(shí)現(xiàn)了協(xié)程,JVM 底層并不支持協(xié)程;Go 天生就是支持協(xié)程的,不支持多進(jìn)程和多線程。Java 的 Project Loom
項(xiàng)目支持協(xié)程,
參考資料
極客時(shí)間-Java性能調(diào)優(yōu)實(shí)戰(zhàn)/19.如何用協(xié)程來(lái)優(yōu)化多線程業(yè)務(wù)?
https://www.cnblogs.com/Survivalist/p/11527949.html
https://www.jianshu.com/p/5db701a764cb
到此這篇關(guān)于深入理解 Java、Kotlin、Go 的線程和協(xié)程的文章就介紹到這了,更多相關(guān) Java、Kotlin、Go 的線程和協(xié)程內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何通過(guò)zuul添加或修改請(qǐng)求參數(shù)
這篇文章主要介紹了如何通過(guò)zuul添加或修改請(qǐng)求參數(shù)的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07SpringBoot使用PageHelper插件實(shí)現(xiàn)Mybatis分頁(yè)效果
這篇文章主要介紹了SpringBoot使用PageHelper插件實(shí)現(xiàn)Mybatis分頁(yè)效果,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的參考借鑒價(jià)值,需要的朋友可以參考下2024-02-02詳解spring boot容器加載完后執(zhí)行特定操作
這篇文章主要介紹了詳解spring boot容器加載完后執(zhí)行特定操作,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-01-01JAVA基于PDF box將PDF轉(zhuǎn)為圖片的實(shí)現(xiàn)方法
這篇文章主要介紹了JAVA基于PDF box將PDF轉(zhuǎn)為圖片的操作方法,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值 ,需要的朋友可以參考下2019-07-07SVN出現(xiàn)提示org.apache.subversion.javahl.ClientException: Attempt
這篇文章主要介紹了SVN出現(xiàn)提示org.apache.subversion.javahl.ClientException: Attempted to lock an already-locked dir解決方案的相關(guān)資料,需要的朋友可以參考下2016-12-12spark中使用groupByKey進(jìn)行分組排序的示例代碼
這篇文章主要介紹了spark中使用groupByKey進(jìn)行分組排序的實(shí)例代碼,本文通過(guò)實(shí)例代碼給大家講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03關(guān)于Spring?Ioc和DI注解的問(wèn)題
這篇文章主要介紹了Spring?Ioc和DI注解,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03