1. jvm線程
JDK1.2之前落竹,程序員們?yōu)镴VM開發(fā)了自己的一個(gè)線程調(diào)度內(nèi)核,而到操作系統(tǒng)層面就是用戶空間內(nèi)的線程實(shí)現(xiàn)。而到了JDK1.2及以后胃榕,JVM選擇了更加穩(wěn)健且方便使用的操作系統(tǒng)原生的線程模型淌喻,通過系統(tǒng)調(diào)用僧家,將程序的線程交給了操作系統(tǒng)內(nèi)核進(jìn)行調(diào)度
對(duì)于JDK來說,Linux版都是使用一對(duì)一的線程模型實(shí)現(xiàn)的裸删,一條Java線程就映射到一條輕量級(jí)進(jìn)程(LWP)之中八拱,N對(duì)M的線程模型稱之為協(xié)程(golong有實(shí)現(xiàn))
- 用戶級(jí)實(shí)現(xiàn)線程:
程序員需要自己實(shí)現(xiàn)線程的數(shù)據(jù)結(jié)構(gòu)、創(chuàng)建銷毀和調(diào)度維護(hù)涯塔。也就相當(dāng)于需要實(shí)現(xiàn)一個(gè)自己的線程調(diào)度內(nèi)核肌稻,而同時(shí)這些線程運(yùn)行在操作系統(tǒng)的一個(gè)進(jìn)程內(nèi)
,最后操作系統(tǒng)直接對(duì)進(jìn)程
進(jìn)行調(diào)度匕荸,線程的調(diào)度只是在用戶態(tài)爹谭,減少了操作系統(tǒng)從內(nèi)核態(tài)到用戶態(tài)的切換開銷,但是某一個(gè)線程進(jìn)行系統(tǒng)調(diào)用時(shí)一個(gè)線程的阻塞會(huì)導(dǎo)致整個(gè)進(jìn)程阻塞榛搔,用戶態(tài)沒有時(shí)鐘中斷機(jī)制诺凡,也就是說一個(gè)線程長(zhǎng)時(shí)間不是放cpu齿风,會(huì)導(dǎo)致該進(jìn)程中其它線程無法獲取cpu時(shí)間片而持續(xù)等待
用戶級(jí)線程則不能享受多處理器, 因?yàn)槎鄠€(gè)用戶級(jí)線程對(duì)應(yīng)到一個(gè)內(nèi)核級(jí)線程上, 一個(gè)內(nèi)核級(jí)線程在同一時(shí)刻只能運(yùn)行在一個(gè)處理器上. 不過, M:N的線程模型畢竟提供了這樣一種手段, 可以讓不需要并行執(zhí)行的線程運(yùn)行在一個(gè)內(nèi)核級(jí)線程對(duì)應(yīng)的若干個(gè)用戶級(jí)線程上, 可以節(jié)省它們的切換開銷,用戶級(jí)線程的切換顯然要比內(nèi)核級(jí)線程的切換快一些, 前者可能只是一個(gè)簡(jiǎn)單的長(zhǎng)跳轉(zhuǎn), 而后者則需要保存/裝載寄存器, 進(jìn)入然后退出內(nèi)核態(tài). (進(jìn)程切換則還需要切換地址空間等.) 進(jìn)行內(nèi)核態(tài)和用戶態(tài)的轉(zhuǎn)換 - 混合實(shí)現(xiàn)M:N模型(用戶線程:LWP不是1:1關(guān)系)
用戶態(tài)實(shí)現(xiàn)線程和LWP同時(shí)存在绑洛,使用內(nèi)核提供的線程調(diào)度功能及處理器映射救斑,用戶線程的系統(tǒng)調(diào)用
要通過輕量級(jí)進(jìn)程來完成,大大降低了整個(gè)進(jìn)程被完全阻塞的風(fēng)險(xiǎn)
linux上真屯,一個(gè)線程默認(rèn)的棧大小是8M脸候,創(chuàng)建幾萬個(gè)線程就壓力山大,所以會(huì)出現(xiàn)協(xié)程 golang默認(rèn)大小2k
pthread_create()創(chuàng)建線程時(shí)绑蔫,若不指定分配堆棧大小运沦,系統(tǒng)會(huì)分配默認(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)建百萬級(jí)的協(xié)程携添。而這些協(xié)程本質(zhì)上還是要依托于具體的操作系統(tǒng)線程去執(zhí)行的。比如說我創(chuàng)建了M個(gè)協(xié)程篓叶,然后在N個(gè)線程上執(zhí)行烈掠,這就是M:N的方案,顯然缸托,Java里是沒有協(xié)程的左敌。當(dāng)然,現(xiàn)在OpenJDK社區(qū)的 loom 項(xiàng)目正在努力為JDK增加協(xié)程俐镐,協(xié)程的切換只有cpu上下文切換開銷的矫限,在用戶態(tài)完成,不需要進(jìn)行特權(quán)切換(用戶態(tài)->內(nèi)核態(tài))佩抹,只是恢復(fù)幾個(gè)寄存器的數(shù)據(jù)叼风,而線程切換除了cpu上下文切換開銷,還要進(jìn)行系統(tǒng)調(diào)用執(zhí)行軟中斷棍苹,此時(shí)要進(jìn)行特權(quán)切換(因?yàn)榫€程調(diào)度是由內(nèi)核來完成的无宿,所以需要進(jìn)入內(nèi)核態(tài)),內(nèi)核態(tài)和用戶態(tài)的切換開銷就很大了
- Java中線程的本質(zhì):
其實(shí)就是操作系統(tǒng)中的線程(LWP廊勃,對(duì)Linux操作系統(tǒng)來說本質(zhì)上還是進(jìn)程)懈贺,Linux下是基于pthread庫(kù)實(shí)現(xiàn)的輕量級(jí)進(jìn)程(NPTL Native POSIX Thread Library) - LWP:LWP是通過clone創(chuàng)建的進(jìn)程,由于LWP和父進(jìn)程會(huì)共享部分資源坡垫,比如地址空間梭灿,文件系統(tǒng),文件句柄冰悠,信號(hào)處理函數(shù)等堡妒,所以把LWP稱為輕量級(jí)進(jìn)程。
這些線程的狀態(tài)時(shí)JVM中的線程狀態(tài)和操作系統(tǒng)中的線程狀態(tài)有映射關(guān)系
2. 操作系統(tǒng)中線程和Java線程狀態(tài)的關(guān)系:
從實(shí)際意義上來講溉卓,操作系統(tǒng)中的線程除去new
和terminated
狀態(tài)皮迟,一個(gè)線程真實(shí)存在的狀態(tài)搬泥,只有:
ready
:線程等待系統(tǒng)調(diào)度分配CPU使用權(quán)。
running
:表示線程獲得了CPU使用權(quán)伏尼,正在進(jìn)行運(yùn)算
waiting
:表示線程等待(或者說掛起)忿檩,讓出CPU資源給其他線程使用,運(yùn)行狀態(tài)下的線程如果調(diào)用阻塞 API爆阶,如阻塞方式讀取文件燥透, 線程狀態(tài)就將變成休眠狀態(tài)。這種情況下辨图,線程將會(huì)讓出 CPU 使用權(quán)班套。休眠結(jié)束,線程狀態(tài)將會(huì)先變成可運(yùn)行狀態(tài)故河。
為什么除去new
和terminated
狀態(tài)吱韭?是因?yàn)檫@兩種狀態(tài)實(shí)際上并不存在于線程運(yùn)行中,所以也沒什么實(shí)際討論的意義鱼的。
**對(duì)于Java中的線程狀態(tài):
無論是Timed Waiting
理盆,Waiting
還是Blocked
,對(duì)應(yīng)的都是操作系統(tǒng)線程的waiting
(等待)狀態(tài)鸳吸。
而Runnable狀態(tài)熏挎,則對(duì)應(yīng)了操作系統(tǒng)中的ready和running狀態(tài)速勇。
3. Linux的NPTL庫(kù)實(shí)現(xiàn)了POSIX標(biāo)準(zhǔn)
1: 查看進(jìn)程列表的時(shí)候, 相關(guān)的一組task_struct應(yīng)當(dāng)被展現(xiàn)為列表中的一個(gè)節(jié)點(diǎn);
2: 發(fā)送給這個(gè)"進(jìn)程"的信號(hào)(對(duì)應(yīng)kill系統(tǒng)調(diào)用), 將被對(duì)應(yīng)的這一組task_struct所共享, 并且被其中的任意一個(gè)"線程"處理;
3: 發(fā)送給某個(gè)"線程"的信號(hào)(對(duì)應(yīng)pthread_kill), 將只被對(duì)應(yīng)的一個(gè)task_struct接收, 并且由它自己來處理;
4: 當(dāng)"進(jìn)程"被停止或繼續(xù)時(shí)(對(duì)應(yīng)SIGSTOP/SIGCONT信號(hào)), 對(duì)應(yīng)的這一組task_struct狀態(tài)將改變;
5: 當(dāng)"進(jìn)程"收到一個(gè)致命信號(hào)(比如由于段錯(cuò)誤收到SIGSEGV信號(hào)), 對(duì)應(yīng)的這一組task_struct將全部退出;
NPTL是Linux 線程庫(kù)的一個(gè)新實(shí)現(xiàn)晌砾,線程組其實(shí)是在task_struct中增加了tgid(thread group id)字段,一般認(rèn)為L(zhǎng)inux通過這種方式支持了線程烦磁,其中進(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)度的實(shí)體。進(jìn)程調(diào)度先誉,嚴(yán)格意義上說應(yīng)該叫LWP調(diào)度湿刽,進(jìn)程調(diào)度,不是以線程組為單位調(diào)度的褐耳,本質(zhì)是以LWP為單位調(diào)度的诈闺。這個(gè)結(jié)論乍一看驚世駭俗,細(xì)細(xì)一想铃芦,其是很合理雅镊。我們?yōu)槭裁炊嗑€程襟雷?因?yàn)槎郈PU,多核仁烹,我們要充分利用多核,同一個(gè)線程組的不同LWP是可以同時(shí)跑在不同的CPU之上的耸弄,因?yàn)檫@個(gè)并發(fā),所以我們有線程鎖的設(shè)計(jì)卓缰,這從側(cè)面證明了叙赚,LWP是調(diào)度的實(shí)體單元
代碼
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
然后平臺(tái)相關(guān)的部分各自不同,以Linux為例的話是
src/os/linux/vm/osThread_linux.hpp
public class Thread {
private long eetop; // 實(shí)為指向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;
- 抽取代碼可以看出這幾個(gè)數(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
ps -ef
ps -efL
可以查看lwp 的 pid tid tgid信息僚饭,查看一個(gè)進(jìn)程多少個(gè)線程
注意ps默認(rèn)只打印進(jìn)程級(jí)別信息震叮,需要用-L選項(xiàng)來查看線程基本信息。
參考
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é)程切換的代價(jià)比線程切換低?