美文网首页
jvm线程&&Linux线程&&协程

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

作者: 邵红晓 | 来源:发表于2020-05-08 15:01 被阅读0次

    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
    为什么协程切换的代价比线程切换低?

    相关文章

      网友评论

          本文标题:jvm线程&&Linux线程&&协程

          本文链接:https://www.haomeiwen.com/subject/wzwxnhtx.html