【修煉內功】[JVM] 細說線程

本文已收錄 【修煉內功】躍遷之路

細說JVM線程狀態.png

自從踏入程序猿這條不歸路,便擺脫不了(進程)線程這隻粘人的小妖精,尤爲在硬件資源「過剩」的今天java

不論你在使用c、C++、.Net,仍是Java、Python、Golang,都免不了要踏過這一關,即便使用以「單線程」著稱的Node.js,也要藉助pm2相似的進程管理工具fork一批進程,來榨乾機器資源linux

早些年使用c編寫多線程時,須要使用宏定義來兼容多平臺下不一樣庫的函數,而Java從一開始便宣稱的"Write Once, Run Anywhere"從虛擬機層面幫咱們屏蔽了衆多平臺差別,那,Java線程與OS線程間有什麼關係?算法

系統架構

細說JVM線程狀態001.png

以*nix類系統爲例,其系統體系架構主要分爲用戶態(user context)內核態(kernel context)segmentfault

內核,本質上講是一種較爲底層的控制計算機硬件資源的軟件windows

用戶態,即上層應用程序的活動空間,應用程序的執行依託於內核提供的資源,爲了使上層資源訪問內核資源,內核提供系統調用接口以供上層應用訪問多線程

系統調用,能夠看做是操做系統的最小功能單元,一種不能再簡化的操做,而函數庫則是對一組系統調用的封裝,以下降應用程序調用內核的複雜度架構

用戶態與內核態切換

在*nix類系統中,爲了有效減小內核資源的訪問及衝突,對不一樣的操做賦予了不一樣的執行等級,越是與系統相關的關鍵操做,越是須要高特權來執行函數

linux操做系統中主要採用了0和3兩個特權等級,分別對應於內核態及用戶態,運行於用戶態的進程能夠執行的操做及訪問的資源會受到很大的限制,而運行在內核態的進程則能夠執行任何操做,而且在資源的訪問上也不會受到任何限制工具

通常應用程序一開始運行時都會處於用戶態,當一些操做須要在內核權限下才能執行時,則會涉及一次從用戶態到內核態的切換過程,當該操做執行完畢後,又會涉及一次從內核態到用戶態的切換過程性能

細說JVM線程狀態002.png

線程模型

回過頭來,從系統層面聊一聊線程的實現模型

用戶線程 v.s. 內核線程

簡單來說

  • 用戶線程

    由應用程序建立、調度、撤銷,不須要內核的支持(內核不感知)

    • 因爲不須要內核的支持,便不涉及用戶態/內核態的切換,消耗的資源較少,速度也較快
    • 因爲須要應用程序控制線程的輪換調度,當有一個用戶線程被阻塞時,整個所屬進程便會被阻塞,同時在多核處理器下只能在一個核內分時複用,不能充分利用多核優點
  • 內核線程

    由內核建立、調用、撤銷,並由內核維護線程的上下文信息及線程切換

    • 因爲內核線程由內核進行維護,當一個內核線程被阻塞時,不會影響其餘線程的正常運行,而且多核處理器下,一個進程內的多個線程能夠充分利用多核的優點同時執行
    • 因爲須要內核進行維護,在線程建立、切換過程當中便會涉及用戶態/內核態的切換,增長系統消耗

輕量級進程 LWP

在linux操做系統中,每每都是經過fork函數建立一個子進程來表明內核中的線程,在fork完一個子進程後,還須要將父進程中大部分的上下文信息複製到子進程中,消耗大量cpu時間用來初始化內存空間,產生大量冗餘數據

爲了不上述狀況,輕量級進程(Light Weight Process, LWP)便出現了,其使用clone系統調用建立子進程,過程當中只將部分父進程數據進行復制,沒有被複制的資源能夠經過指針進行數據共享,這樣一來LWP的運行單元更小、運行速度更快

LWP與內核線程一一映射,每一個LWP都由一個內核線程支持

1:1 線程模型

1:1 模型,即每個用戶線程都對應一個內核線程,每一個線程的建立、調度、銷燬都須要內核的支持,每次線程的建立、切換都會設計用戶狀態/內核狀態的切換,性能開銷比較大,而且單個進程可以建立的LWP的數量是有限的,但可以充分裏用多核的優點

細說線程狀態.003.jpeg

N:1 線程模型

N:1模型,即全部的用戶線程都會對應到一個內核線程中,該模型能夠在用戶空間完成線程的建立、調度、銷燬,不須要內核的支持,一樣也就不涉及用戶狀態/內核狀態的切換,線程的操做較快且消耗較低,而且線程數量不受操做系統的限制,但不能發揮多核的優點,只能在一個核中分時複用,而且因爲內核不能感知用戶態的線程,在某一線程被阻塞時,會致使整個所屬進程阻塞

細說線程狀態.004.jpeg

N:M 線程模型

N:M 模型是基於以上兩種模型的一種混合實現,多個用戶線程對應於多個內核線程,即解決了1:1模型中性能開銷及線程數量的問題,也解決了N:1模型中阻塞問題,同時也能充分利用CPU的多核優點,這也是大部分協程實現的基礎

細說線程狀態.005.jpeg

Java在1.2以前基於用戶線程實現(N:1線程模型),在1.2以後windows及linux平臺下采用1:1線程模型,在solaris平臺使用1:1或N:M線程模型實現(可配置)

線程狀態

如下以linux平臺爲例

linux平臺下,JVM採用1:1的線程模型,那Java中的線程狀態與OS的線程狀態是否也是一一對應的?

系統線程狀態&生命週期

細說線程狀態.006.jpeg

linux系統的線程狀態及生命週期如上圖,每種狀態的詳細解釋再也不一一贅述,這裏簡單介紹下RUNNABLERUNNING

  • RUNNABLE

    線程處於可運行的狀態,但尚未被系統調度器選中,即尚未分配到CPU時間片

  • RUNNING

    線程處於運行狀態,即線程分配到了時間片,正在執行機器指令

Java線程狀態&生命週期

Java中的線程狀態並無使用系統線程狀態一一對應的方式,而是提供了與之不一樣的6種狀態

如下,linux系統線程狀態會使用 斜體 加以區分

細說線程狀態.007.jpeg

linux系統中的RUNNABLERUNNING被Java合併成了RUNNABLE一種狀態,而linux系統中的BLOCKED被Java細化成了WAITINGTIMED_WAITINGBLOCKED三種狀態

Java中的線程狀態與系統中的線程狀態大致類似,但又略有不一樣,最明顯的一點是,若是因爲I/O阻塞會使Java線程進入BLOCKED狀態麼?NO!I/O阻塞在系統層面會使線程進入BLOCKED狀態,但在Java裏線程狀態依然是RUNNABLE

系統中的RUNNABLE表示線程正在等待CPU資源,在在Java中被認爲一樣是在運行中,只是在排隊等待而已,故Java中將系統的RUNNABLERUNNING合併成了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 timeout
    • Thread.join with no timeout
    • LockSupport.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 call Object.notify() or Object.notifyAll() on that object. A thread that has called Thread.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 timeout
    • Thread.join with timeout
    • LockSupport.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時也會發生線程間的上下文切換

即,多線程的上下文切換實際上就是由多線程兩個運行狀態的互相切換致使的

那,什麼狀況下會觸發 RUNNINGBLOCKEDRUNNABLE (對應Java中 RUNNABLEBLOCKED/WAITING/TIMED_WAITINGRUNNABLE) 的狀態轉變呢?

一種爲程序自己觸發,一種爲操做系統或虛擬機觸發

程序自己觸發很容易理解,全部會致使 RUNNABLEBLOCKED/WAITING/TIMED_WAITING 的邏輯均會觸發線程間上下文切換,如synchronizedwaitjoinparksleep

操做系統觸發,最多見的好比線程時間片的分配

虛擬機觸發,最多見的在於進行垃圾回收時的 'stop the world'

如何優化

既然全部會致使 RUNNABLEBLOCKED/WAITING/TIMED_WAITING 的邏輯均會觸發線程間上下文切換,那便從誘因入手

鎖競爭

鎖其實並非性能開銷的根源,競爭鎖纔是

  1. 減小鎖的持有時間

    鎖的持有時間越長,就意味着可能有越多的線程在等待鎖的釋放,若是是同步鎖,除了會形成線程間上下文切換外,還會有進程間的上下文切換 (mutex lock)

    優化方法有不少,好比將synchronized關鍵字從方法修飾移到方法體內,將synchronized修飾的代碼塊中無關的邏輯移到synchronized代碼塊外,等等

  2. 下降鎖的粒度

    • 鎖分離

      對於讀操做大於寫操做的邏輯,能夠將傳統的同步鎖拆分爲讀寫鎖,即讀鎖與寫鎖,在多線程中,只有讀寫與寫寫是互斥的,避免讀讀狀況下鎖的競爭

    • 鎖分段

      對於大集合或者大對象的鎖操做,能夠考慮將鎖進一步分離,將大集合或者大對象分隔成多個段,對每個段分別上鎖,以免對不一樣段進行操做時鎖的競爭,如ConcurrentHashMap中對鎖的實現

  3. 非阻塞樂觀鎖代替競爭鎖

    • 使用volatile

      volatile 的讀寫操做不會致使上下文切換,開銷較小,但volatile只保證可見性,不保證原子性

    • 使用CAS

      CAS 是一個原子的 if-then-act 操做,能夠在我外部鎖的狀況下來保證讀寫操做的一致性,如Atomic包中的算法

    • 其它非阻塞樂觀鎖

wait/notify優化

  • 使用notify()代替notifyAll()

    衆所周知,notifyAll會喚醒全部相關的線程,而notify則會喚醒指定線程,以減小過多不相關線程的上下文切換

  • 使用Lock+Condition組合的方式替代wait/notify

    synchronized是基於系統層面實現的,而Lock則是應用程序層面實現的,不會形成用戶態/內核態的切換

    Condition會避免相似notifyAll提早喚醒過多無關線程的問題

合理設置線程池大小

線程池數量不宜設置過大,線程池數量設置過大容易致使大量線程處於等待CPU時間片的狀態(RUNNABLE),同時也會致使過多的上下文切換

使用協程實現非阻塞等待

協程能夠看作是一種輕量級線程

前文介紹到,Java線程使用1:1線程模型,每一個用戶線程都會映射到一個系統線程,線程由內核來管理

協程則使用N:M線程模型,協程徹底由應用程序來管理,避免了衆多的上下文切換

(協程不等於沒有系統線程,只是會大大減小系統線程上下文切換的次數)

總結

  • 操做系統體系架構主要分爲用戶態(user context)內核態(kernel context)
  • 因爲系統操做分不一樣的執行等級,應用程序在執行一些高等級操做時會發生用戶態/內核態的切換
  • 用戶線程由應用程序建立、調度、撤銷,不須要內核的支持
  • 內核線程由內核建立、調用、撤銷,並由內核維護線程的上下文信息及線程切換
  • 線程模型分爲1:1N:1N:M三種,Java在window及linux上採用1:1線程模型,即每一個用戶線程都會對應一個內核線程
  • Java中的線程狀態並無使用系統線程狀態一一對應的方式,而是使用NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED六種狀態
  • 用戶態/內核態的切換會致使進程間上下文切換
  • 多線程兩個運行狀態的互相切換會致使線程間的上下文切換,諸如synchronized wait join park sleep 等常見操做均會引發線程間的上下文切換
  • 理解線程上下文切換的緣由,合理優化程序,減小上下文切換,減輕系統負擔

訂閱號

相關文章
相關標籤/搜索