jvm線程&&Linux線程&&協(xié)程

1. jvm線程

JDK1.2之前,程序員們?yōu)镴VM開發(fā)了自己的一個線程調(diào)度內(nèi)核,而到操作系統(tǒng)層面就是用戶空間內(nèi)的線程實現(xiàn)。而到了JDK1.2及以后,JVM選擇了更加穩(wěn)健且方便使用的操作系統(tǒng)原生的線程模型,通過系統(tǒng)調(diào)用,將程序的線程交給了操作系統(tǒng)內(nèi)核進(jìn)行調(diào)度
對于JDK來說,Linux版都是使用一對一的線程模型實現(xiàn)的,一條Java線程就映射到一條輕量級進(jìn)程(LWP)之中,N對M的線程模型稱之為協(xié)程(golong有實現(xiàn))

  • 用戶級實現(xiàn)線程:
    程序員需要自己實現(xiàn)線程的數(shù)據(jù)結(jié)構(gòu)、創(chuàng)建銷毀和調(diào)度維護(hù)。也就相當(dāng)于需要實現(xiàn)一個自己的線程調(diào)度內(nèi)核,而同時這些線程運行在操作系統(tǒng)的一個進(jìn)程內(nèi),最后操作系統(tǒng)直接對進(jìn)程進(jìn)行調(diào)度,線程的調(diào)度只是在用戶態(tài),減少了操作系統(tǒng)從內(nèi)核態(tài)到用戶態(tài)的切換開銷,但是某一個線程進(jìn)行系統(tǒng)調(diào)用時一個線程的阻塞會導(dǎo)致整個進(jìn)程阻塞,用戶態(tài)沒有時鐘中斷機(jī)制,也就是說一個線程長時間不是放cpu,會導(dǎo)致該進(jìn)程中其它線程無法獲取cpu時間片而持續(xù)等待
    用戶級線程則不能享受多處理器, 因為多個用戶級線程對應(yīng)到一個內(nèi)核級線程上, 一個內(nèi)核級線程在同一時刻只能運行在一個處理器上. 不過, M:N的線程模型畢竟提供了這樣一種手段, 可以讓不需要并行執(zhí)行的線程運行在一個內(nèi)核級線程對應(yīng)的若干個用戶級線程上, 可以節(jié)省它們的切換開銷,用戶級線程的切換顯然要比內(nèi)核級線程的切換快一些, 前者可能只是一個簡單的長跳轉(zhuǎn), 而后者則需要保存/裝載寄存器, 進(jìn)入然后退出內(nèi)核態(tài). (進(jìn)程切換則還需要切換地址空間等.) 進(jìn)行內(nèi)核態(tài)和用戶態(tài)的轉(zhuǎn)換
  • 混合實現(xiàn)M:N模型(用戶線程:LWP不是1:1關(guān)系)
    用戶態(tài)實現(xiàn)線程和LWP同時存在,使用內(nèi)核提供的線程調(diào)度功能及處理器映射,用戶線程的系統(tǒng)調(diào)用要通過輕量級進(jìn)程來完成,大大降低了整個進(jìn)程被完全阻塞的風(fēng)險
    linux上,一個線程默認(rèn)的棧大小是8M,創(chuàng)建幾萬個線程就壓力山大,所以會出現(xiàn)協(xié)程 golang默認(rèn)大小2k
    pthread_create()創(chuàng)建線程時,若不指定分配堆棧大小,系統(tǒng)會分配默認(rèn)值
    不指定-Xss jvm給Java棧定義的"系統(tǒng)默認(rèn)"大小是1MB,jvm使用linux默認(rèn)的棧大小
$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 515488
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 65535
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 4096
virtual memory          (kbytes, -v) unlimited

協(xié)程:使用棧內(nèi)存是按需使用的,所以可以隨隨便便創(chuàng)建百萬級的協(xié)程。而這些協(xié)程本質(zhì)上還是要依托于具體的操作系統(tǒng)線程去執(zhí)行的。比如說我創(chuàng)建了M個協(xié)程,然后在N個線程上執(zhí)行,這就是M:N的方案,顯然,Java里是沒有協(xié)程的。當(dāng)然,現(xiàn)在OpenJDK社區(qū)的 loom 項目正在努力為JDK增加協(xié)程,協(xié)程的切換只有cpu上下文切換開銷的,在用戶態(tài)完成,不需要進(jìn)行特權(quán)切換(用戶態(tài)->內(nèi)核態(tài)),只是恢復(fù)幾個寄存器的數(shù)據(jù),而線程切換除了cpu上下文切換開銷,還要進(jìn)行系統(tǒng)調(diào)用執(zhí)行軟中斷,此時要進(jìn)行特權(quán)切換(因為線程調(diào)度是由內(nèi)核來完成的,所以需要進(jìn)入內(nèi)核態(tài)),內(nèi)核態(tài)和用戶態(tài)的切換開銷就很大了

  • Java中線程的本質(zhì):
    其實就是操作系統(tǒng)中的線程(LWP,對Linux操作系統(tǒng)來說本質(zhì)上還是進(jìn)程),Linux下是基于pthread庫實現(xiàn)的輕量級進(jìn)程(NPTL Native POSIX Thread Library)
  • LWP:LWP是通過clone創(chuàng)建的進(jìn)程,由于LWP和父進(jìn)程會共享部分資源,比如地址空間,文件系統(tǒng),文件句柄,信號處理函數(shù)等,所以把LWP稱為輕量級進(jìn)程。
    JVM中的線程生命周期

    這些線程的狀態(tài)時JVM中的線程狀態(tài)和操作系統(tǒng)中的線程狀態(tài)有映射關(guān)系

2. 操作系統(tǒng)中線程和Java線程狀態(tài)的關(guān)系:

從實際意義上來講,操作系統(tǒng)中的線程除去newterminated狀態(tài),一個線程真實存在的狀態(tài),只有:
ready:線程等待系統(tǒng)調(diào)度分配CPU使用權(quán)。
running:表示線程獲得了CPU使用權(quán),正在進(jìn)行運算
waiting:表示線程等待(或者說掛起),讓出CPU資源給其他線程使用,運行狀態(tài)下的線程如果調(diào)用阻塞 API,如阻塞方式讀取文件, 線程狀態(tài)就將變成休眠狀態(tài)。這種情況下,線程將會讓出 CPU 使用權(quán)。休眠結(jié)束,線程狀態(tài)將會先變成可運行狀態(tài)。
為什么除去newterminated狀態(tài)?是因為這兩種狀態(tài)實際上并不存在于線程運行中,所以也沒什么實際討論的意義。
**對于Java中的線程狀態(tài):
無論是Timed WaitingWaiting還是Blocked,對應(yīng)的都是操作系統(tǒng)線程的waiting(等待)狀態(tài)。
而Runnable狀態(tài),則對應(yīng)了操作系統(tǒng)中的ready和running狀態(tài)。

3. Linux的NPTL庫實現(xiàn)了POSIX標(biāo)準(zhǔn)

1: 查看進(jìn)程列表的時候, 相關(guān)的一組task_struct應(yīng)當(dāng)被展現(xiàn)為列表中的一個節(jié)點;
2: 發(fā)送給這個"進(jìn)程"的信號(對應(yīng)kill系統(tǒng)調(diào)用), 將被對應(yīng)的這一組task_struct所共享, 并且被其中的任意一個"線程"處理;
3: 發(fā)送給某個"線程"的信號(對應(yīng)pthread_kill), 將只被對應(yīng)的一個task_struct接收, 并且由它自己來處理;
4: 當(dāng)"進(jìn)程"被停止或繼續(xù)時(對應(yīng)SIGSTOP/SIGCONT信號), 對應(yīng)的這一組task_struct狀態(tài)將改變;
5: 當(dāng)"進(jìn)程"收到一個致命信號(比如由于段錯誤收到SIGSEGV信號), 對應(yīng)的這一組task_struct將全部退出;

NPTL是Linux 線程庫的一個新實現(xiàn),線程組其實是在task_struct中增加了tgid(thread group id)字段,一般認(rèn)為Linux通過這種方式支持了線程,其中進(jìn)程的tgid等于自己的pid,線程的tgid等于進(jìn)程的pid。
Linux 進(jìn)程(線程組)是共享虛擬內(nèi)存的

  • 用戶態(tài)pthread_create出來的線程,在內(nèi)核態(tài),也擁有自己的進(jìn)程描述符task_struct(copy_process里面調(diào)用dup_task_struct創(chuàng)建)。這是什么意思呢。意思是我們用戶態(tài)所說的線程,一樣是內(nèi)核進(jìn)程調(diào)度的實體。進(jìn)程調(diào)度,嚴(yán)格意義上說應(yīng)該叫LWP調(diào)度,進(jìn)程調(diào)度,不是以線程組為單位調(diào)度的,本質(zhì)是以LWP為單位調(diào)度的。這個結(jié)論乍一看驚世駭俗,細(xì)細(xì)一想,其是很合理。我們?yōu)槭裁炊嗑€程?因為多CPU,多核,我們要充分利用多核,同一個線程組的不同LWP是可以同時跑在不同的CPU之上的,因為這個并發(fā),所以我們有線程鎖的設(shè)計,這從側(cè)面證明了,LWP是調(diào)度的實體單元

代碼

Thread.start()#start0()
/src/share/vm/prims/jvm.cpp

private native void start0();

static JNINativeMethod methods[] = {
    {"start0",           "()V",        (void *)&JVM_StartThread},
    {"stop0",            "(" OBJ ")V", (void *)&JVM_StopThread},
    {"isAlive",          "()Z",        (void *)&JVM_IsThreadAlive},
    {"suspend0",         "()V",        (void *)&JVM_SuspendThread},
    {"resume0",          "()V",        (void *)&JVM_ResumeThread},
    {"setPriority0",     "(I)V",       (void *)&JVM_SetThreadPriority},
    {"yield",            "()V",        (void *)&JVM_Yield},
    {"sleep",            "(J)V",       (void *)&JVM_Sleep},
    {"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},
    {"countStackFrames", "()I",        (void *)&JVM_CountStackFrames},
    {"interrupt0",       "()V",        (void *)&JVM_Interrupt},
    {"isInterrupted",    "(Z)Z",       (void *)&JVM_IsInterrupted},
    {"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},
    {"getThreads",        "()[" THD,   (void *)&JVM_GetAllThreads},
    {"dumpThreads",      "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
    {"setNativeName",    "(" STR ")V", (void *)&JVM_SetNativeThreadName},
};

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
native_thread = new JavaThread(&thread_entry, sz);

Java層面 | HotSpot VM層面 | 操作系統(tǒng)層面

java.lang.Thread | JavaThread -> OSThread | native thread
src/share/vm/runtime/thread.hpp
src/share/vm/runtime/osThread.hpp
然后平臺相關(guān)的部分各自不同,以Linux為例的話是
src/os/linux/vm/osThread_linux.hpp

public class Thread {  
  private long        eetop; // 實為指向JavaThread的指針  
}
thread.hpp  
class JavaThread: public Thread {}; 
class Thread: public ThreadShadow {  
  // 指向OSThread的指針  
  OSThread* _osthread;  // Platform-specific thread information  
};  
osThread.hpp
class OSThread: public CHeapObj<mtThread> 
osThread_linux.hpp
pthread_t _pthread_id;
  • 抽取代碼可以看出這幾個數(shù)據(jù)結(jié)構(gòu)的關(guān)系是:
    java.lang.Thread thread;
    JavaThread* jthread = thread->_eetop;
    OSThread* osthread = jthread->_osthread;
    pthread_t pthread_id = osthread->_pthread_id;
  • 內(nèi)核view和用戶view thread
    image.png

    ps -ef
    ps -efL 可以查看lwp 的 pid tid tgid信息,查看一個進(jìn)程多少個線程
    注意ps默認(rèn)只打印進(jìn)程級別信息,需要用-L選項來查看線程基本信息。

參考
https://blog.csdn.net/CringKong/article/details/79994511
https://blog.csdn.net/mm_hh/java/article/details/72587207
https://www.zhihu.com/question/263955521
http://blog.chinaunix.net/uid-24774106-id-3650136.html
為什么協(xié)程切換的代價比線程切換低?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容