• 1.4 线程的核心原理


    1.4 线程的核心原理

    现代操作系统(如Windows、Linux、Solaris)提供了强大的线程管理能力,Java不需要再进行独立的线程管理和调度,而是将线程调度工作委托给操作系统的调度进程去完成。在某些系统(比如Solaris操作系统)上,JVM甚至将每个Java线程一对一地对应到操作系统的本地线程,彻底将线程调度委托给操作系统。

    1.4.1 线程的调度与时间片

    由于CPU的计算频率非常高,每秒计算数十亿次,因此可以将CPU的时间从毫秒的维度进行分段,每一小段叫作一个CPU时间片。

    目前操作系统中主流的线程调度方式是:基于CPU时间片方式进行线程调度。线程只有得到CPU时间片才能执行指令,处于执行状态,没有得到时间片的线程处于就绪状态,等待系统分配下一个CPU时间片。由于时间片非常短,在各个线程之间快速地切换,因此表现出来的特征是很多个线程在“同时执行”或者“并发执行”。

    线程的调度模型目前主要分为两种:分时调度模型和抢占式调度模型。

    (1)分时调度模型:系统平均分配CPU的时间片,所有线程轮流占用CPU,即在时间片调度的分配上所有线程“人人平等”。

    2)抢占式调度模型:系统按照线程优先级分配CPU时间片。优先级高的线程优先分配CPU时间片,如果所有就绪线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些。

    由于目前大部分操作系统都是使用抢占式调度模型进行线程调度,Java的线程管理和调度是委托给操作系统完成的,与之相对应,Java的线程调度也是使用抢占式调度模型,因此Java的线程都有优先级。

    1.4.2 线程的优先级

    在Thread类中有一个实例属性和两个实例方法,专门用于进行线程优先级相关的操作。与线程优先级相关的成员属性为:


         private int priority;//该属性保存一个Thread实例的优先级,即1~10的值
    
    • 1

    与Thread类线程优先级相关的实例方法为:

    方法1:public final int getPriority(),获取线程优先级。

    方法2:public final void setPriority(int priority),设置线程优先级。

    Thread实例的priority属性默认是级别5,对应的类常量是NORM_PRIORITY。优先级最大值为10,最小值为1,Thread类中定义的三个优先级常量如下:


         public static final int MIN_PRIORITY = 1;
         public static final int NORM_PRIORITY = 5;
         public static final int MAX_PRIORITY = 10;
    
    • 1
    • 2
    • 3

    Java中使用抢占式调度模型进行线程调度。priority实例属性的优先级越高,线程获得CPU时间片的机会就越多,但也不是绝对的。

    使用Thread类的stop()实例方法,该方法的作用是终止线程的执行。

     for (int i = 0; i < threads.length; i++) {
          threads[i].stop();
     }
    
    • 1
    • 2
    • 3

    Thread类的stop()实例方法是一个过时的方法,也是一个不安全的方法。这里的安全指的是系统资源(文件、网络连接等)的安全——stop()实例方法可能导致资源状态不一致,或者说资源出现问题时很难定位。在实际开发过程中,不建议使用stop()实例方法。

    (1)整体而言,高优先级的线程获得的执行机会更多。从实例中可以看到:优先级在6级以上的线程和4级以下的线程执行机会明显偏多,整体对比非常明显。

    (2)执行机会的获取具有随机性,优先级高的不一定获得的机会多。

    1.4.3 线程的生命周期

    Java中线程的生命周期分为6种状态。Thread类有一个实例属性和一个实例方法专门用于保存和获取线程的状态。其中,用于保存线程Thread实例状态的实例属性为:


         private int threadStatus;//以整数的形式保存线程的状态
    
    • 1

    Thread类用于获取线程状态的实例方法为:


         public Thread.State getState(); //返回当前线程的执行状态,一个枚举类型值
    
    • 1

    Thread.State是一个内部枚举类,定义了6个枚举常量,分别代表Java线程的6种状态,具体如下:


         public static enum State {
             NEW,                               //新建
             RUNNABLE,                  //可执行:包含操作系统的就绪、运行两种状态
             BLOCKED,                   //阻塞
             WAITING,                   //等待
             TIMED_WAITING,             //限时等待
             TERMINATED;                //终止
         }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在Thread.State定义的6种状态中,有4种是比较常见的状态,它们是:NEW(新建)状态、RUNNABLE(可执行)状态、TERMINATED(终止)状态、TIMED_WAITING(限时等待)状态。

    1.NEW状态

    Java源码对NEW状态的说明是:创建成功但是没有调用start()方法启动的Thread线程实例都处于NEW状态。

    当然,并不是Thread线程实例的start()方法一经调用,其状态就从NEW状态到RUNNABLE状态,此时并不意味着线程立即获取CPU时间片并且立即执行,中间需要一系列操作系统的内部操作。

    2.RUNNABLE状态

    前面讲到,当调用了Thread实例的start()方法后,下一步如果线程获取CPU时间片开始执行,JVM将异步调用线程的run()方法执行其业务代码。那么在run()方法被异步调用之前,JVM做了哪些事情呢?

    JVM的幕后工作和操作系统的线程调度有关。Java中的线程管理是通过JNI本地调用的方式委托操作系统的线程管理API完成的。当Java线程的Thread实例的start()方法被调用后,操作系统中的对应线程进入的并不是运行状态,而是就绪状态,而Java线程并没有这个就绪状态。操作系统中线程的就绪状态是什么状态的呢?

    JVM的线程状态与其幕后的操作系统线程状态之间的转换关系简化后如图1-11所示。

    image-20220802145452590

    一个操作系统线程如果处于就绪状态,就表示“万事俱备,只欠东风”,即该线程已经满足执行条件,但是还不能执行。处于就绪状态的线程需要等待系统的调度,一旦就绪状态被系统选中,获得CPU时间片,线程就开始占用CPU,开始执行线程的代码,这时线程的操作系统状态发生了改变,进入了运行状态。

    在操作系统中,处于运行状态的线程在CPU时间片用完之后,又回到就绪状态,等待CPU的下一次调度。就这样,操作系统线程在就绪状态和执行状态之间被系统反复地调度,这种情况会一直持续,直到线程的代码逻辑执行完成或者异常终止。这时线程的操作系统状态又发生了改变,进入线程的最后状态——TERMINATED状态。

    就绪状态和运行状态都是操作系统中的线程状态。在Java语言中,并没有细分这两种状态,而是将这两种状态合并成同一种状态——RUNNABLE状态。因此,在Thread.State枚举类中,没有定义线程的就绪状态和运行状态,只是定义了RUNNABLE状态。这就是Java线程状态和操作系统中线程状态不同的地方。

    总之,NEW状态的Thread实例调用了start()方法后,线程的状态将变成RUNNABLE状态。尽管如此,线程的run()方法不一定会马上被并发执行,需要在线程获取了CPU时间片之后才真正启动并发执行。

    3.TERMINATED状态

    处于RUNNABLE状态的线程在run()方法执行完成之后就变成终止状态TERMINATED了。当然,如果在run()方法执行过程中发生了运行时异常而没有被捕获,run()方法将被异常终止,线程也会变成TERMINATED状态。

    4.TIMED_WAITING状态

    线程处于一种特殊的等待状态,准确地说,线程处于限时等待状态。能让线程处于限时等待状态的操作大致有以下几种:

    (1)Thread.sleep(int n):使得当前线程进入限时等待状态,等待时间为n毫秒。

    (2)Object.wait():带时限的抢占对象的monitor锁。

    (3)Thread.join():带时限的线程合并。

    (4)LockSupport.parkNanos():让线程等待,时间以纳秒为单位。

    (5)LockSupport.parkUntil():让线程等待,时间可以灵活设置。

    1.5.1 线程名称的设置和获取

    在Thread类中可以通过构造器Thread(…)初始化设置线程名称,也可以通过setName(…)实例方法设置线程名称,取得线程名称可以通过getName()方法完成。

    关于线程名称有以下几个要点:

    (1)线程名称一般在启动线程前设置,但也允许为运行的线程设置名称。

    (2)允许两个Thread对象有相同的名称,但是应该避免。

    (3)如果程序没有为线程指定名称,系统会自动为线程设置名称。

    说明

    编程规范要求:创建线程或线程池时,需要指定有意义的线程名称,方便出错时回溯。

    1.5.2 线程的sleep操作

    sleep的作用是让目前正在执行的线程休眠,让CPU去执行其他的任务。从线程状态来说,就是从执行状态变成限时阻塞状态。Sleep()方法定义在Thread类中,是一组静态方法,有两个重载版本:


         //使目前正在执行的线程休眠millis毫秒
         public static void sleep(long millis) throws InterruptException;
         
         //使目前正在执行的线程休眠millis毫秒,nanos纳秒
         public static void sleep(long millis,int nanos) throws InterruptException;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    sleep()方法会有InterruptException受检异常抛出,如果调用了sleep()方法,就必须进行异常审查,捕获InterruptedException异常,或者再次通过方法声明存在InterruptedException异常。

    1.5.3 线程的interrupt操作

    Java语言提供了stop()方法终止正在运行的线程,但是Java将Thread的stop()方法设置为过时,不建议大家使用。为什么呢?因为使用stop()方法是很危险的,就像突然关闭计算机电源,而不是按正常程序关机。在程序中,我们是不能随便中断一个线程的,我们无法知道这个线程正运行在什么状态,它可能持有某把锁,强行中断线程可能导致锁不能释放的问题;或者线程可能在操作数据库,强行中断线程可能导致数据不一致的问题。正是由于调用stop()方法来终止线程可能会产生不可预料的结果,因此不推荐调用stop()方法。

    一个线程什么时候可以退出呢?当然只有线程自己才能知道。所以,这里介绍一下Thread的interrupt()方法,此方法本质不是用来中断一个线程,而是将线程设置为中断状态。

    当我们调用线程的interrupt()方法时,它有两个作用:

    (1)如果此线程处于阻塞状态(如调用了Object.wait()方法),就会立马退出阻塞,并抛出InterruptedException异常,线程就可以通过捕获InterruptedException来做一定的处理,然后让线程退出。更确切地说,如果线程被Object.wait()、Thread.join()和Thread.sleep()三种方法之一阻塞,此时调用该线程的interrupt()方法,该线程将抛出一个InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早终结被阻塞状态。

    (2)如果此线程正处于运行之中,线程就不受任何影响,继续运行,仅仅是线程的中断标记被设置为true。所以,程序可以在适当的位置通过调用isInterrupted()方法来查看自己是否被中断,并执行退出操作。

    说明

    如果线程的interrupt()方法先被调用,然后线程开始调用阻塞方法进入阻塞状态,InterruptedException异常依旧会抛出。如果线程捕获InterruptedException异常后,继续调用阻塞方法,将不再触发InterruptedException异常。

    1.5.4 线程的join操作

    线程的合并是一个比较难以说清楚的概念,什么是线程的合并呢?举一个例子,假设有两个线程A和B。现在线程A在执行过程中对另一个线程B的执行有依赖,具体的依赖为:线程A需要将线程B的执行流程合并到自己的执行流程中(至少表面如此),这就是线程合并,被动方线程B可以叫作被合并线程。这个例子中的线程A合并线程B的伪代码大致为:


             class ThreadA extends Thread
             {
                 void run()
                 {
                     Thread threadb = new Thread("thread-b");
                     threadb.join();
                 }
             }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    1.线程的join操作的三个版本

    join()方法是Thread类的一个实例方法,有三个重载版本:


         //重载版本1:此方法会把当前线程变为TIMED_WAITING,直到被合并线程执行结束
         public final void join() throws InterruptedException:
         
         //重载版本2:此方法会把当前线程变为TIMED_WAITING,直到被合并线程执行结束,或者等待被合并线程执行millis的时间
         public final synchronized void join(long millis) throws InterruptedException:
         
         //重载版本3:此方法会把当前线程变为TIMED_WAITING,直到被合并线程执行结束,或者等待被合并线程执行millis+nanos的时间
         public final synchroinzed void join(long millis, int nanos) throws InterruptedException:
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    调用join()方法的要点:

    (1)join()方法是实例方法,需要使用被合并线程的句柄(或者指针、变量)去调用,如threadb.join()。执行threadb.join()这行代码的当前线程为合并线程(甲方),进入TIMED_WAITING等待状态,让出CPU。

    (2)如果设置了被合并线程的执行时间millis(或者millis+nanos),并不能保证当前线程一定会在millis时间后变为RUNNABLE。

    (3)如果主动方合并线程在等待时被中断,就会抛出InterruptedException受检异常。

    说明

    调用join()方法的语句可以理解为合并点,合并的本质是:线程A需要在合并点等待,一直等到线程B执行完成,或者等待超时。

    为了方便表达,本书将依赖的线程A叫作甲方线程,被依赖的线程B叫作乙方线程。简单理解线程合并就是甲方线程调用乙方线程的join()方法,在执行流程上将乙方线程合并到甲方线程。甲方线程等待乙方线程执行完成后,甲方线程再继续执行,如图1-12所示。

    如果乙方线程无限制长时间地执行,甲方线程可以进行限时等待:甲方线程等待乙方线程执行一定时间后,如果乙方还没有完成,甲方线程再继续执行。

    调用join()方法的优势是比较简单,劣势是join()方法没有办法直接取得乙方线程的执行结果。

    image-20220802161251484

    3.join线程的WAITING状态

    线程的WAITING(等待)状态表示线程在等待被唤醒。处于WAITING状态的线程不会被分配CPU时间片。执行以下两个操作,当前线程将处于WAITING状态:

    (1)执行没有时限(timeout)参数的thread.join()调用:在线程合并场景中,若线程A调用B.join()去合入B线程,则在B执行期间线程A处于WAITING状态,一直等线程B执行完成。

    (2)执行没有时限(timeout)参数的object.wait()调用:指一个拥有object对象锁的线程,进入相应的代码临界区后,调用相应的object的wait()方法去等待其“对象锁”(Object Monitor)上的信号,若“对象锁”上没有信号,则当前线程处于WAITING状态,如图1-13所示。

    image-20220802161523288

    4.join线程的TIMED_WAITING状态

    线程的TIMED_WAITING状态表示在等待唤醒。处于TIMED_WAITING状态的线程不会被分配CPU时间片,它们要等待被唤醒,或者直到等待的时限到期。

    在线程合入场景中,若线程A在调用B.join()操作时加入了时限参数,则在B执行期间线程A处于TIMED_WAITING状态。若B在等待时限内没有返回,则线程A结束等待TIMED_WAITING状态,恢复成RUNNABLE状态。

    1.5.5 线程的yield操作

    线程的yield(让步)操作的作用是让目前正在执行的线程放弃当前的执行,让出CPU的执行权限,使得CPU去执行其他的线程。处于让步状态的JVM层面的线程状态仍然是RUNNABLE状态,但是该线程所对应的操作系统层面的线程从状态上来说会从执行状态变成就绪状态。线程在yield时,线程放弃和重占CPU的时间是不确定的,可能是刚刚放弃CPU,马上又获得CPU执行权限,重新开始执行。

    yield()方法是Thread类提供的一个静态方法,它可以让当前正在执行的线程暂停,但它不会阻塞该线程,只是让线程转入就绪状态。yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次,yield()方法只有一个版本:

    总结起来,Thread.yeid()方法有以下特点:

    (1)yield仅能使一个线程从运行状态转到就绪状态,而不是阻塞状态。

    (2)yield不能保证使得当前正在运行的线程迅速转换到就绪状态。

    (3)即使完成了迅速切换,系统通过线程调度机制从所有就绪线程中挑选下一个执行线程时,就绪的线程有可能被选中,也有可能不被选中,其调度的过程受到其他因素(如优先级)的影响。

    1.5.6 线程的daemon操作

    Java中的线程分为两类:守护线程与用户线程。守护线程也称为后台线程,专门指在程序进程运行过程中,在后台提供某种通用服务的线程。比如,每启动一个JVM进程,都会在后台运行一系列的GC(垃圾回收)线程,这些GC线程就是守护线程,提供幕后的垃圾回收服务。

    1.守护线程的基本操作

    在Thread类中,有一个实例属性和两个实例方法,专门用于进行守护线程相关的操作。

    (1)实例属性daemon:保存一条Thread线程实例的守护状态,默认为false,表示线程默认为用户线程。


         private boolean daemon = false;
    
    • 1

    (2)实例方法setDaemon(…):此方法将线程标记为守护线程或者用户线程。setDaemon(true)将线程设置为守护线程,setDaemon(false)将线程设置为用户线程。


         public final void setDaemon(boolean on);
    
    • 1

    (3)实例方法isDaemon():获取线程的守护状态,用于判断该线程是不是守护线程。


         public final boolean isDaemon();
    
    • 1

    3.守护线程与用户线程的关系

    从是否为守护线程的角度,对Java线程进行分类,分为用户线程和守护线程。守护线程和用户线程的本质区别是:二者与JVM虚拟机进程终止的方向不同。用户线程和JVM进程是主动关系,如果用户线程全部终止,JVM虚拟机进程也随之终止;守护线程和JVM进程是被动关系,如果JVM进程终止,所有的守护线程也随之终止,如图1-14所示。

    image-20220802162238347

    图1-14 守护线程与用户线程的关系

    换个角度来理解,守护线程提供服务,是守护者,用户线程享受服务,是被守护者。只有用户线程全部终止了,相当于没有了被守护者,守护线程也就没有工作可做了,也就可以全部终止了。当然,用户线程全部终止,JVM进程也就没有继续的必要了。反过来说,只要有一个用户线程没有终止,JVM进程也不会退出。

    但是在终止维度上,守护线程和JVM进程没有主动关系。也就是说,哪怕是守护线程全部被终止,JVM虚拟机也不一定终止。

    4.守护线程的要点

    使用守护线程时,有以下几点需要特别注意:

    (1)守护线程必须在启动前将其守护状态设置为true,启动之后不能再将用户线程设置为守护线程,否则JVM会抛出一个InterruptedException异常。

    具体来说,如果线程为守护线程,就必须在线程实例的start()方法调用之前调用线程实例的setDaemon(true),设置其daemon实例属性值为true。

    (2)守护线程存在被JVM强行终止的风险,所以在守护线程中尽量不去访问系统资源,如文件句柄、数据库连接等。守护线程被强行终止时,可能会引发系统资源操作不负责任的中断,从而导致资源不可逆的损坏。

    (3)守护线程创建的线程也是守护线程。

    在守护线程中创建的线程,新的线程都是守护线程。在创建之后,如果通过调用setDaemon(false)将新的线程显式地设置为用户线程,新的线程可以调整成用户线程。

    1.5.7 线程状态总结

    接下来,对线程的6种状态以及各种状态的进入条件做一个总结。

    1.NEW状态

    通过new Thread(…)已经创建线程,但尚未调用start()启动线程,该线程处于NEW(新建)状态。虽然前面介绍了4种方式创建线程,但是其中的其他三种方式本质上都是通过new Thread()创建线程,仅仅是创建了不同的target执行目标实例(如Runnable实例)。

    2.RUNNABLE状态

    Java把Ready(就绪)和Running(执行)两种状态合并为一种状态:RUNNABLE(可执行)状态(或者可运行状态)。调用了线程的start()实例方法后,线程就处于就绪状态。此线程获取到CPU时间片后,开始执行run()方法中的业务代码,线程处于执行状态。

    (1)就绪状态

    就绪状态仅仅表示线程具备运行资格,如果没有被操作系统的调度程序挑选中,线程就永远处于就绪状态。当前线程进入就绪状态的条件大致包括以下几种:

    ·调用线程的start()方法,此线程就会进入就绪状态。

    ·当前线程的执行时间片用完。

    ·线程睡眠(Sleep)操作结束。

    ·对其他线程合入(Join)操作结束。

    ·等待用户输入结束。

    ·线程争抢到对象锁(Object Monitor)。

    ·当前线程调用了yield()方法出让CPU执行权限。

    (2)执行状态

    线程调度程序从就绪状态的线程中选择一个线程,被选中的线程状态将变成执行状态。这也是线程进入执行状态的唯一方式。

    3.BLOCKED状态

    处于BLOCKED(阻塞)状态的线程并不会占用CPU资源,以下情况会让线程进入阻塞状态:

    (1)线程等待获取锁

    等待获取一个锁,而该锁被其他线程持有,则该线程进入阻塞状态。当其他线程释放了该锁,并且线程调度器允许该线程持有该锁时,该线程退出阻塞状态。

    (2)IO阻塞

    线程发起了一个阻塞式IO操作后,如果不具备IO操作的条件,线程就会进入阻塞状态。IO包括磁盘IO、网络IO等。IO阻塞的一个简单例子:线程等待用户输入内容后继续执行。

    说明

    网络IO阻塞的原理以及Java高性能IO编程的核心知识可参阅另一本书《Java高并发核心编程 卷1:NIO、Netty、Redis、ZooKeeper》。

    4.WAITING状态

    处于WAITING(无限期等待)状态的线程不会被分配CPU时间片,需要被其他线程显式地唤醒,才会进入就绪状态。线程调用以下3种方法会让自己进入无限等待状态:

    ·Object.wait()方法,对应的唤醒方式为:Object.notify()/Object.notifyAll()。

    ·Thread.join()方法,对应的唤醒方式为:被合入的线程执行完毕。

    ·LockSupport.park()方法,对应的唤醒方式为:LockSupport.unpark(Thread)。

    5.TIMED_WAITING状态

    处于TIMED_WAITING(限时等待)状态的线程不会被分配CPU时间片,如果指定时间之内没有被唤醒,限时等待的线程会被系统自动唤醒,进入就绪状态。以下3种方法会让线程进入限时等待状态:

    ·Thread.sleep(time)方法,对应的唤醒方式为:sleep睡眠时间结束。

    ·Object.wait(time)方法,对应的唤醒方式为:调用Object.notify()/Object.notifyAll()主动唤醒,或者限时结束。

    ·LockSupport.parkNanos(time)/parkUntil(time)方法,对应的唤醒方式为:线程调用配套的LockSupport.unpark(Thread)方法结束,或者线程停止(park)时限结束。

    进入BLOCKED状态、WAITING状态、TIMED_WAITING状态的线程都会让出CPU的使用权;另外,等待或者阻塞状态的线程被唤醒后,进入Ready状态,需要重新获取时间片才能接着运行。

    6.TERMINATED状态

    时间之内没有被唤醒,限时等待的线程会被系统自动唤醒,进入就绪状态。以下3种方法会让线程进入限时等待状态:

    ·Thread.sleep(time)方法,对应的唤醒方式为:sleep睡眠时间结束。

    ·Object.wait(time)方法,对应的唤醒方式为:调用Object.notify()/Object.notifyAll()主动唤醒,或者限时结束。

    ·LockSupport.parkNanos(time)/parkUntil(time)方法,对应的唤醒方式为:线程调用配套的LockSupport.unpark(Thread)方法结束,或者线程停止(park)时限结束。

    进入BLOCKED状态、WAITING状态、TIMED_WAITING状态的线程都会让出CPU的使用权;另外,等待或者阻塞状态的线程被唤醒后,进入Ready状态,需要重新获取时间片才能接着运行。

    6.TERMINATED状态

    线程结束任务之后,将会正常进入TERMINATED(死亡)状态;或者说在线程执行过程中发生了异常(而没有被处理),也会导致线程进入死亡状态。

  • 相关阅读:
    使用容器运行nginx及docker命令介绍
    【夜读】自我提升的8个好习惯,迷茫时看一看
    设计模式-代理模式Proxy
    优化器算法
    Pyppeteer中文文档
    英语连词总结
    Linux系统文件属性
    Real3D FlipBook jQuery Plugin 3.41 Crack
    tcp三次握手四次挥手
    vue3-基础知识(0) - vue2和vue3基本比较
  • 原文地址:https://blog.csdn.net/KongZhongNiao/article/details/126125187