白话说Java虚拟机原理系列【第八章】:线程的实现详解

文章目录

    • 线程的实现
        • 使用内核线程实现
        • 使用用户线程实现
        • 使用用户线程加内核线程混合实现
    • Java线程的实现


前导说明:
本文基于《深入理解Java虚拟机》第二版和个人理解完成,
以大白话的形式期望能用大家都看懂的描述讲清楚虚拟机内幕,
后续会增加基于《深入理解Java虚拟机》第三版内容,并进行二个版本的对比

线程的实现

线程是一个可以共用进程资源的调度执行单位。线程也可以被独立调度(线程是CPU调度的基本单位)。

我们简单了解下一下场景:

  • 假设1cpu单核处理器:这种情况当执行线程时会直接调度线程去运行,注意进程只是运行线程的载体,线程可以共享进程的资源,但进程本身没有运行能力,而一个核心的处理器,遇到并发时,会通过时间片分时调度,通过内核控制在不同的线程间切换/挂起执行,达到“准”并发现象。
  • 假设1cpu多核处理器:一个核心和上述一样,只不过多个核心可以同时处理不同的线程,而不同核心同时处理线程,叫做并行,并行被处理的线程也是真正的并发线程,并行执行的是处理器,并发的是线程,这样达到真正的并发场景。
  • 假设多cpu多核操作系统(多CPU必然是由操作系统调度):之所以有多cpu是因为服务器主板的支持,实际底层运行仍然是1cpu多核原理。

注意理解此处的一些概念关系。

使用内核线程实现

  • 1.内核线程(Kernel-Level Thread,KLT):
    就是由操作系统内核来支持的线程,并且由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。即所有操作都是需要内核来完成。
  • 2.轻量级进程(Light Weight Process,LWP):
    程序一般不直接使用内核线程,而是使用轻量级进程来间接操控(内核提供的接口)内核线程。其实轻量级进程就是我们平常所说的线程了。
  • 3.一对一线程模型:
    使用时,一个轻量级进程就要有一个内核线程的支持,即他们的关系是1:1的,所以又叫做一比一线程模型。
  • 4.缺点:由于轻量级进程是由内核线程支持的,也即都需要内核来操作,而程序一般都是用户态进程,所以需要用户态和内核态之间来回切换(不是cpu的调度切换,注意区分),而这种切换是要消耗内核资源的(如内核线程的占空间),因此一个系统支持轻量级进程的数量是有限的。

使用用户线程实现

  • 1.用户线程是完全建立在用户空间的线程库上,系统内核不能感知线程的实现。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。
  • 2.如果程序实现得当,这种线程不需要切换到内核态,因此操作可以是非常快速和低消耗的,也可以支持规模更大的线程数量。
  • 3.一对多线程模型:
    这种进程与用户线程1:N的关系称为一对多的线程模型。
  • 4.缺点1:由于没有系统内核的支持,所以所有线程操作都需要用户程序自己处理。线程的创建、切换和调度都是需要考虑的问题。
  • 5.缺点2:由于操作系统只把处理器资源分配到进程,那诸如"阻塞如何处理"、“多处理器系统如何将线程映射到其他处理器上”,这些问题都很难处理,所以现在用户线程几乎没有使用的价值了。

使用用户线程加内核线程混合实现

  • 1.因为内核线程其实又叫轻量级进程,所以用户和内核线程混合实现也可叫做用户线程加轻量级进程混合实现。
  • 2.这种实现就是用户线程和轻量级进程混合搭配,互相利用优点,用户线程仍然是完全建立在用户空间,因此用户线程的创建、切换等操作依然高效,且可以支持大规模的用户线程并发,而由内核线程支持的轻量级进程则可以作为补足用户线程无法通过内核进行线程调度和处理器映射的短板,即用户线程的调度和处理器映射都通过轻量级进程来完成。
  • 3.多对多的线程模型
    这种实现中用户线程和轻量级进程的数量不定,即为N:M的关系,所以称为多对多的线程模型。

Java线程的实现

上一章我们简单看了下操作系统线程的实现,这里我们看一下在Java中的线程实现和操作系统的线程实现是否一样以及有哪些联系。

我们知道Java是通过Thread类来实现线程的,即调用start()方法即可启动一个线程,同时我们也知道Thread类的很多方法都是native方法,所以他的底层还是借用了操作系统的线程实现方式来实现的。

  • Java的线程模型采用基于操作系统采用的线程模型来实现,即操作系统是什么模型他就用什么模型。注意,采用哪种线程模型只跟线程的并发规模和操作成本产生影响,对于Java程序的编码和运行过程来说,这些差异都是透明的。
  • Sun JDK采用一对一线程模型,即一个java线程就映射到一条轻量级进程。
    • 1.window操作系统的线程模型,采用的就是一对一线程模型,所以windows上的JVM就是采用一对一的线程模型。
    • 2.linux操作系统的线程模型,同windows一样也是采用的一对一线程模型。
    • 3.Solaris系统,提供了一对一和多对多两种方法,所以Solaris平台的JVM也提供了虚拟机参数来决定使用一对一还是多对多模型。参数:
      • -XX:UseLWPSynchronization(默认值,多对多线程模型)
      • -XX:UseBoundTreads(一对一线程模型)
  • Java线程调度:操作系统为线程分配处理器使用权,有处理器运行线程。其中java提供了2种线程调度方式,如下:
    • 协同式线程调度
      • 1.线程的执行时间由线程本身来控制,线程把自己执行完后,要主动通知系统切换到另外一个线程上。
      • 2.优点:实现简单,能让线程一直执行完才交出处理器上下文,避免了上下文切换带来的同步问题等。
      • 3.缺点:如果一个线程出了问题,始终没有通知系统切换,那么就会一直阻塞,可能导致整个系统崩溃。
    • 抢占式线程调度
      • 1.每个线程的执行时间将由系统来分配。这样就不会有协同式调度的问题了。Java就是采用的此种调度方案。
      • 2.虽然程序不能决定线程的分配了,但是可以通过线程的优先级,来让系统更多的去分配处理器去执行优先级高的线程。
      • 3.Java线程的优先级共提供了10个级别。当两个线程同时处于Ready状态时,优先级越高的线程越容易被系统选择执行。
        白话说Java虚拟机原理系列【第八章】:线程的实现详解_第1张图片
        从操作系统的角度分析线程的优先级共有6中,而Java中提供了10种,虽然个数不同,但从图中可以看出,windows中线程的某个优先可可能在Java中会细化成2种优先级,只不过是更细化了些,这里了解一下即可。
      • 4.注意:不能太依赖于优先级
        比如windows系统中存在一个称为"优先级推进器"的功能(可以关闭此功能),它的功能就是当系统发现某个线程运行的比较勤奋时,就可能会越过优先级的设置,即便优先级低,也会去为它多分配执行时间。
  • 线程状态:
    白话说Java虚拟机原理系列【第八章】:线程的实现详解_第2张图片
    • 1.新建(NEW):新创建了一个线程对象。
    • 2.可运行(RUNNABLE)
      线程对象创建后,其他线程(比如main线程)调用了该线程对象的start()方法。此时线程为可运行状态,等待内核调度以获取cpu的使用权。
    • 3.运行(RUNNING)
      可运行状态(runnable)的线程获得了cpu 时间片(timeslice) 开始执行程序代码,此时即为运行状态。
    • 4.阻塞(BLOCKED)
      阻塞状态是指线程因为某种原因放弃了cpu使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice转到运行(running)状态。阻塞的情况分三种:也即让线程停下来的三种类型。
      • (一)等待阻塞:运行(running)的线程执行o.wait()方法(o为锁对象),JVM会把该线程放入等待队列(waitting queue)中,当锁对象调用o.notify()方法后,会唤醒等待的线程,使其进入可运行状态;
      • (二)同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中,当其他线程释放锁,而锁等待池中的线程得到了锁,那此线程进入可运行状态;
      • (三)其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
    • 5.死亡(DEAD)
      线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
  • 关于释放cpu和释放锁的方法:
    • sleep()方法会进入阻塞让出cpu,不会释放锁,即便cpu又一次切换到该线程,也不会执行,因为阻塞了,直到sleep时间完成,才会进入runnable,此时如果有cpu切换才会执行。
    • yield()方法会让出cpu并进入runnable,不会释放锁,所以只要cpu又切换回来,那么它将直接执行。
    • join()方法,是一个顺序问题,调用t.join()的线程,将会等待t线程执行完毕后,进入runnable状态,再等待cpu时间片,等到后直接运行。join方法源码中是通过wait来实现的,因为wait是释放锁的,所以join也是要释放锁。

你可能感兴趣的:(Java虚拟机原理,java,jvm,linux,线程)