jvm线程&&Linux线程&&协程

1. jvm线程

JDK1.2之前,程序员们为JVM开发了自己的一个线程调度内核,而到操作系统层面就是用户空间内的线程实现。而到了JDK1.2及以后,JVM选择了更加稳健且方便使用的操作系统原生的线程模型,通过系统调用,将程序的线程交给了操作系统内核进行调度
对于JDK来说,Linux版都是使用一对一的线程模型实现的,一条Java线程就映射到一条轻量级进程(LWP)之中,N对M的线程模型称之为协程(golong有实现)

  • 用户级实现线程:
    程序员需要自己实现线程的数据结构、创建销毁和调度维护。也就相当于需要实现一个自己的线程调度内核,而同时这些线程运行在操作系统的一个进程内,最后操作系统直接对进程进行调度,线程的调度只是在用户态,减少了操作系统从内核态到用户态的切换开销,但是某一个线程进行系统调用时一个线程的阻塞会导致整个进程阻塞,用户态没有时钟中断机制,也就是说一个线程长时间不是放cpu,会导致该进程中其它线程无法获取cpu时间片而持续等待
    用户级线程则不能享受多处理器, 因为多个用户级线程对应到一个内核级线程上, 一个内核级线程在同一时刻只能运行在一个处理器上. 不过, M:N的线程模型毕竟提供了这样一种手段, 可以让不需要并行执行的线程运行在一个内核级线程对应的若干个用户级线程上, 可以节省它们的切换开销,用户级线程的切换显然要比内核级线程的切换快一些, 前者可能只是一个简单的长跳转, 而后者则需要保存/装载寄存器, 进入然后退出内核态. (进程切换则还需要切换地址空间等.) 进行内核态和用户态的转换
  • 混合实现M:N模型(用户线程:LWP不是1:1关系)
    用户态实现线程和LWP同时存在,使用内核提供的线程调度功能及处理器映射,用户线程的系统调用要通过轻量级进程来完成,大大降低了整个进程被完全阻塞的风险
    linux上,一个线程默认的栈大小是8M,创建几万个线程就压力山大,所以会出现协程 golang默认大小2k
    pthread_create()创建线程时,若不指定分配堆栈大小,系统会分配默认值
    不指定-Xss jvm给Java栈定义的"系统默认"大小是1MB,jvm使用linux默认的栈大小
$ 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

协程:使用栈内存是按需使用的,所以可以随随便便创建百万级的协程。而这些协程本质上还是要依托于具体的操作系统线程去执行的。比如说我创建了M个协程,然后在N个线程上执行,这就是M:N的方案,显然,Java里是没有协程的。当然,现在OpenJDK社区的 loom 项目正在努力为JDK增加协程,协程的切换只有cpu上下文切换开销的,在用户态完成,不需要进行特权切换(用户态->内核态),只是恢复几个寄存器的数据,而线程切换除了cpu上下文切换开销,还要进行系统调用执行软中断,此时要进行特权切换(因为线程调度是由内核来完成的,所以需要进入内核态),内核态和用户态的切换开销就很大了

  • Java中线程的本质:
    其实就是操作系统中的线程(LWP,对Linux操作系统来说本质上还是进程),Linux下是基于pthread库实现的轻量级进程(NPTL Native POSIX Thread Library)
  • LWP:LWP是通过clone创建的进程,由于LWP和父进程会共享部分资源,比如地址空间,文件系统,文件句柄,信号处理函数等,所以把LWP称为轻量级进程。
    JVM中的线程生命周期

    这些线程的状态时JVM中的线程状态和操作系统中的线程状态有映射关系

2. 操作系统中线程和Java线程状态的关系:

从实际意义上来讲,操作系统中的线程除去newterminated状态,一个线程真实存在的状态,只有:
ready:线程等待系统调度分配CPU使用权。
running:表示线程获得了CPU使用权,正在进行运算
waiting:表示线程等待(或者说挂起),让出CPU资源给其他线程使用,运行状态下的线程如果调用阻塞 API,如阻塞方式读取文件, 线程状态就将变成休眠状态。这种情况下,线程将会让出 CPU 使用权。休眠结束,线程状态将会先变成可运行状态。
为什么除去newterminated状态?是因为这两种状态实际上并不存在于线程运行中,所以也没什么实际讨论的意义。
**对于Java中的线程状态:
无论是Timed WaitingWaiting还是Blocked,对应的都是操作系统线程的waiting(等待)状态。
而Runnable状态,则对应了操作系统中的ready和running状态。

3. Linux的NPTL库实现了POSIX标准

1: 查看进程列表的时候, 相关的一组task_struct应当被展现为列表中的一个节点;
2: 发送给这个"进程"的信号(对应kill系统调用), 将被对应的这一组task_struct所共享, 并且被其中的任意一个"线程"处理;
3: 发送给某个"线程"的信号(对应pthread_kill), 将只被对应的一个task_struct接收, 并且由它自己来处理;
4: 当"进程"被停止或继续时(对应SIGSTOP/SIGCONT信号), 对应的这一组task_struct状态将改变;
5: 当"进程"收到一个致命信号(比如由于段错误收到SIGSEGV信号), 对应的这一组task_struct将全部退出;

NPTL是Linux 线程库的一个新实现,线程组其实是在task_struct中增加了tgid(thread group id)字段,一般认为Linux通过这种方式支持了线程,其中进程的tgid等于自己的pid,线程的tgid等于进程的pid。
Linux 进程(线程组)是共享虚拟内存的

  • 用户态pthread_create出来的线程,在内核态,也拥有自己的进程描述符task_struct(copy_process里面调用dup_task_struct创建)。这是什么意思呢。意思是我们用户态所说的线程,一样是内核进程调度的实体。进程调度,严格意义上说应该叫LWP调度,进程调度,不是以线程组为单位调度的,本质是以LWP为单位调度的。这个结论乍一看惊世骇俗,细细一想,其是很合理。我们为什么多线程?因为多CPU,多核,我们要充分利用多核,同一个线程组的不同LWP是可以同时跑在不同的CPU之上的,因为这个并发,所以我们有线程锁的设计,这从侧面证明了,LWP是调度的实体单元

代码

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层面 | 操作系统层面

java.lang.Thread | JavaThread -> OSThread | native thread
src/share/vm/runtime/thread.hpp
src/share/vm/runtime/osThread.hpp
然后平台相关的部分各自不同,以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;
  • 抽取代码可以看出这几个数据结构的关系是:
    java.lang.Thread thread;
    JavaThread* jthread = thread->_eetop;
    OSThread* osthread = jthread->_osthread;
    pthread_t pthread_id = osthread->_pthread_id;
  • 内核view和用户view thread
    image.png

    ps -ef
    ps -efL 可以查看lwp 的 pid tid tgid信息,查看一个进程多少个线程
    注意ps默认只打印进程级别信息,需要用-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
为什么协程切换的代价比线程切换低?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355