本文已收錄 【修煉內功】躍遷之路
自從踏入程序猿這條不歸路,便擺脫不了(進程)線程這隻粘人的小妖精,尤爲在硬件資源「過剩」的今天java
不論你在使用c、C++、.Net,仍是Java、Python、Golang,都免不了要踏過這一關,即便使用以「單線程」著稱的Node.js,也要藉助pm2相似的進程管理工具fork一批進程,來榨乾機器資源linux
早些年使用c編寫多線程時,須要使用宏定義來兼容多平臺下不一樣庫的函數,而Java從一開始便宣稱的"Write Once, Run Anywhere"從虛擬機層面幫咱們屏蔽了衆多平臺差別,那,Java線程與OS線程間有什麼關係?算法
以*nix類系統爲例,其系統體系架構主要分爲用戶態(user context)和內核態(kernel context)segmentfault
內核,本質上講是一種較爲底層的控制計算機硬件資源的軟件windows
用戶態,即上層應用程序的活動空間,應用程序的執行依託於內核提供的資源,爲了使上層資源訪問內核資源,內核提供系統調用接口以供上層應用訪問多線程
系統調用,能夠看做是操做系統的最小功能單元,一種不能再簡化的操做,而函數庫則是對一組系統調用的封裝,以下降應用程序調用內核的複雜度架構
在*nix類系統中,爲了有效減小內核資源的訪問及衝突,對不一樣的操做賦予了不一樣的執行等級,越是與系統相關的關鍵操做,越是須要高特權來執行函數
linux操做系統中主要採用了0和3兩個特權等級,分別對應於內核態及用戶態,運行於用戶態的進程能夠執行的操做及訪問的資源會受到很大的限制,而運行在內核態的進程則能夠執行任何操做,而且在資源的訪問上也不會受到任何限制工具
通常應用程序一開始運行時都會處於用戶態,當一些操做須要在內核權限下才能執行時,則會涉及一次從用戶態到內核態的切換過程,當該操做執行完畢後,又會涉及一次從內核態到用戶態的切換過程性能
回過頭來,從系統層面聊一聊線程的實現模型
簡單來說
用戶線程
由應用程序建立、調度、撤銷,不須要內核的支持(內核不感知)
內核線程
由內核建立、調用、撤銷,並由內核維護線程的上下文信息及線程切換
在linux操做系統中,每每都是經過fork函數建立一個子進程來表明內核中的線程,在fork完一個子進程後,還須要將父進程中大部分的上下文信息複製到子進程中,消耗大量cpu時間用來初始化內存空間,產生大量冗餘數據
爲了不上述狀況,輕量級進程(Light Weight Process, LWP)便出現了,其使用clone系統調用建立子進程,過程當中只將部分父進程數據進行復制,沒有被複制的資源能夠經過指針進行數據共享,這樣一來LWP的運行單元更小、運行速度更快
LWP與內核線程一一映射,每一個LWP都由一個內核線程支持
1:1 模型,即每個用戶線程都對應一個內核線程,每一個線程的建立、調度、銷燬都須要內核的支持,每次線程的建立、切換都會設計用戶狀態/內核狀態的切換,性能開銷比較大,而且單個進程可以建立的LWP的數量是有限的,但可以充分裏用多核的優點
N:1模型,即全部的用戶線程都會對應到一個內核線程中,該模型能夠在用戶空間完成線程的建立、調度、銷燬,不須要內核的支持,一樣也就不涉及用戶狀態/內核狀態的切換,線程的操做較快且消耗較低,而且線程數量不受操做系統的限制,但不能發揮多核的優點,只能在一個核中分時複用,而且因爲內核不能感知用戶態的線程,在某一線程被阻塞時,會致使整個所屬進程阻塞
N:M 模型是基於以上兩種模型的一種混合實現,多個用戶線程對應於多個內核線程,即解決了1:1模型中性能開銷及線程數量的問題,也解決了N:1模型中阻塞問題,同時也能充分利用CPU的多核優點,這也是大部分協程實現的基礎
Java在1.2以前基於用戶線程實現(N:1線程模型),在1.2以後windows及linux平臺下采用1:1線程模型,在solaris平臺使用1:1或N:M線程模型實現(可配置)
如下以linux平臺爲例
linux平臺下,JVM採用1:1的線程模型,那Java中的線程狀態與OS的線程狀態是否也是一一對應的?
linux系統的線程狀態及生命週期如上圖,每種狀態的詳細解釋再也不一一贅述,這裏簡單介紹下RUNNABLE與RUNNING
線程處於可運行的狀態,但尚未被系統調度器選中,即尚未分配到CPU時間片
線程處於運行狀態,即線程分配到了時間片,正在執行機器指令
Java中的線程狀態並無使用系統線程狀態一一對應的方式,而是提供了與之不一樣的6種狀態
如下,linux系統線程狀態會使用 斜體 加以區分
linux系統中的RUNNABLE
與RUNNING
被Java合併成了RUNNABLE
一種狀態,而linux系統中的BLOCKED
被Java細化成了WAITING
、TIMED_WAITING
及BLOCKED
三種狀態
Java中的線程狀態與系統中的線程狀態大致類似,但又略有不一樣,最明顯的一點是,若是因爲I/O阻塞會使Java線程進入BLOCKED
狀態麼?NO!I/O阻塞在系統層面會使線程進入BLOCKED
狀態,但在Java裏線程狀態依然是RUNNABLE
!
系統中的RUNNABLE
表示線程正在等待CPU資源,在在Java中被認爲一樣是在運行中,只是在排隊等待而已,故Java中將系統的RUNNABLE
與RUNNING
合併成了RUNNABLE
一種狀態
而對於系統中I/O阻塞引發的BLOCKED
狀態,在Java中被認爲一樣是在等待一種資源,故也認爲是RUNNABLE
的一種狀況
Java線程的狀態在Thread.State
枚舉中能夠查看,其每種狀態的釋義寫的很是清楚,這裏再也不一一解釋
NEW
Thread state for a thread which has not yet started.
RUNNABLE
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.
BLOCKED
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
Object.wait
.
WAITING
Thread state for a waiting thread. A thread is in the waiting state due to calling one of the following methods:
Object.wait
with no timeoutThread.join
with no timeoutLockSupport.park
A thread in the waiting state is waiting for another thread to perform a particular action. For example, a thread that has called
Object.wait()
on an object is waiting for another thread to callObject.notify()
orObject.notifyAll()
on that object. A thread that has calledThread.join()
is waiting for a specified thread to terminate.
TIMED_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:
Thread.sleep
Object.wait
with timeoutThread.join
with timeoutLockSupport.parkNanos
LockSupport.parkUntil
TERMINATED
Thread state for a terminated thread. The thread has completed execution.
上下文切換涉及到進程間上下文切換與線程間上下文切換
用戶態與內核態的每一次切換都會致使進程間上限文的切換,好比java中在使用重量級鎖的時候會依賴系統底層的mutex lock
,而該系統操做會致使用戶態/內核態的切換,進而引發進程間的上下文切換
這裏重點討論下線程間的上下文切換
一個線程由RUNNING
轉爲BLOCKED
時(線程暫停),系統會保存線程的上下文信息
當該線程由BLOCKED
轉爲RUNNABLE
時(線程喚醒),系統會獲取上次的上下文信息以保證線程可以繼續執行
以上的一個過程線程上下文的一次切換過程
一樣,一個線程由RUNNING
轉爲RUNNABLE
,再由RUNNABLE
轉爲RUNNING
時也會發生線程間的上下文切換
即,多線程的上下文切換實際上就是由多線程兩個運行狀態的互相切換致使的
那,什麼狀況下會觸發 RUNNING
→ BLOCKED
→ RUNNABLE
(對應Java中 RUNNABLE
→ BLOCKED
/WAITING
/TIMED_WAITING
→ RUNNABLE
) 的狀態轉變呢?
一種爲程序自己觸發,一種爲操做系統或虛擬機觸發
程序自己觸發很容易理解,全部會致使 RUNNABLE
→ BLOCKED
/WAITING
/TIMED_WAITING
的邏輯均會觸發線程間上下文切換,如synchronized
、wait
、join
、park
、sleep
等
操做系統觸發,最多見的好比線程時間片的分配
虛擬機觸發,最多見的在於進行垃圾回收時的 'stop the world'
既然全部會致使 RUNNABLE
→ BLOCKED
/WAITING
/TIMED_WAITING
的邏輯均會觸發線程間上下文切換,那便從誘因入手
鎖其實並非性能開銷的根源,競爭鎖纔是
鎖的持有時間越長,就意味着可能有越多的線程在等待鎖的釋放,若是是同步鎖,除了會形成線程間上下文切換外,還會有進程間的上下文切換 (mutex lock
)
優化方法有不少,好比將synchronized
關鍵字從方法修飾移到方法體內,將synchronized
修飾的代碼塊中無關的邏輯移到synchronized
代碼塊外,等等
下降鎖的粒度
對於讀操做大於寫操做的邏輯,能夠將傳統的同步鎖拆分爲讀寫鎖,即讀鎖與寫鎖,在多線程中,只有讀寫與寫寫是互斥的,避免讀讀狀況下鎖的競爭
對於大集合或者大對象的鎖操做,能夠考慮將鎖進一步分離,將大集合或者大對象分隔成多個段,對每個段分別上鎖,以免對不一樣段進行操做時鎖的競爭,如ConcurrentHashMap
中對鎖的實現
非阻塞樂觀鎖代替競爭鎖
volatile 的讀寫操做不會致使上下文切換,開銷較小,但volatile只保證可見性,不保證原子性
CAS 是一個原子的 if-then-act 操做,能夠在我外部鎖的狀況下來保證讀寫操做的一致性,如Atomic包中的算法
衆所周知,notifyAll會喚醒全部相關的線程,而notify則會喚醒指定線程,以減小過多不相關線程的上下文切換
synchronized是基於系統層面實現的,而Lock則是應用程序層面實現的,不會形成用戶態/內核態的切換
Condition會避免相似notifyAll提早喚醒過多無關線程的問題
線程池數量不宜設置過大,線程池數量設置過大容易致使大量線程處於等待CPU時間片的狀態(RUNNABLE
),同時也會致使過多的上下文切換
協程能夠看作是一種輕量級線程
前文介紹到,Java線程使用1:1線程模型,每一個用戶線程都會映射到一個系統線程,線程由內核來管理
協程則使用N:M線程模型,協程徹底由應用程序來管理,避免了衆多的上下文切換
(協程不等於沒有系統線程,只是會大大減小系統線程上下文切換的次數)
1:1
、N:1
、N:M
三種,Java在window及linux上採用1:1線程模型,即每一個用戶線程都會對應一個內核線程synchronized
wait
join
park
sleep
等常見操做均會引發線程間的上下文切換