Hey, Nice to meet You. 

必有过人之节.人情有所不能忍者,匹夫见辱,拔剑而起,挺身而斗,此不足为勇也,天下有大勇者,猝然临之而不惊,无故加之而不怒.此其所挟持者甚大,而其志甚远也.          ☆☆☆所谓豪杰之士,

线程的生命周期

我们知道JVM的线程调度策略是优先级抢占式调度。即是指能够大概率的让优先级高的线程抢占到CPU资源(注意并不是优先高的先执行,执行是随机的,只是抢占的概率会大很多,后面的线程优先级会有举例),如果线程的优先级相同,那么就随机选择一个线程,使其占用CPU资源。所以当一个线程创建并且启动之后,它不会一直处于执行状态,那么线程在未取得CPU资源时是处于就绪状态,也会因为某些原因导致线程进入阻塞状态。所以线程状态会多次在运行、就绪和阻塞之间来回切换。

1、线程的生命周期

1.1 传统线程模型的五种线程状态

传统线程模型中把线程的生命周期描述为五种状态:新建(New)就绪(Runnable)运行(Running)阻塞(Blocked)死亡(Dead)。CPU需要在多条线程之间切换,于是线程状态会多次在运行、阻塞、就绪之间切换。

image

通过上面的图各种状态一目了然,线程的生命周期包含的5个阶段:新建、就绪、运行、阻塞、死亡。

  • 新建(new Thread):当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。例如:Thread t1=new Thread()。
  • 就绪(runnable):就是Thread实例调用了start()方法后,线程已经被启动,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行。例如:t1.start()。
  • 运行(running):当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能。
  • 阻塞(blocked):线程在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态。
  • 死亡(dead):如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源。分为自然终止和异常终止。

其中值得一提的是阻塞(Blocked)这个状态:线程在Running的过程中可能会遇到各种阻塞(Blocked)情况,如下:

  1. 等待阻塞:调用运行线程的wait()方法,虚拟机会把该线程放入等待池(wait blocked pool)。如需将阻塞状态下的线程唤醒,可调用notify或者notifyAll()方法,线程被唤醒被放到锁定池(lock blocked pool ),释放同步锁使线程回到就绪行状态(Runnable)。
  2. 锁定(同步)阻塞:运行线程获取对象的同步锁时,该锁已被其他线程获得,虚拟机会把该线程放入锁定池(lock blocked pool )。
  3. 其他阻塞:调用运行线程的sleep()方法、join()方法或线程发出I/O请求时,进入阻塞状态。

1.2 JDK定义的六种线程状态(了解)

java.lang.Thread类内部定义了一个枚举类用来描述线程的六种状态:

    public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }

跟传统线程模型中的线程状态不同的是:

  1. 枚举类中没有区分就绪运行状态,而是定义成了一种状态Runnable
    • 因为对于Java对象来说,只能标记为可运行,至于什么时候运行,不是JVM来控制的了,是OS来进行调度的,而且时间非常短暂,因此对于Java对象的状态来说,无法区分。只能我们人为的进行想象和理解。
  2. 传统模型中的阻塞状态在枚举类的定义中又细分为了三种状态的:BLOCKEDWAITINGTIMED_WAITING
    • BLOCKED:是指互有竞争关系的几个线程,其中一个线程占有锁对象时,其他线程只能等待锁。只有获得锁对象的线程才能有执行机会。
    • TIMED_WAITING:当前线程执行过程中遇到Thread类的sleep或join,Object类的wait,LockSupport类的park方法,并且在调用这些方法时,设置了时间,那么当前线程会进入TIMED_WAITING,直到时间到,或被中断。
    • WAITING:当前线程执行过程中遇到遇到Object类的wait,Thread类的join,LockSupport类的park方法,并且在调用这些方法时,没有指定时间,那么当前线程会进入WAITING状态,直到被唤醒。
      • 通过Object类的wait进入WAITING状态的要有Object的notify/notifyAll唤醒;
      • 通过Condition的await进入WAITING状态的要有Conditon的signal方法唤醒;
      • 通过LockSupport类的park方法进入WAITING状态的要有LockSupport类的unpark方法唤醒
      • 通过Thread类的join进入WAITING状态,只有调用join方法的线程对象结束才能让当前线程恢复;

说明:当从WAITINGTIMED_WAITING恢复到Runnable状态时,如果发现当前线程没有得到监视器锁,那么会立刻转入BLOCKED状态。

2、线程中常用方法

线程中常用方法:

(1)、Thread类中的方法:

  • currentThread():返回对当前正在执行的线程对象的引用。
  • getId():返回此线程的标识符。
  • getName():返回此线程的名称。
  • setName(String name):设置此线程的名称。
  • getPriority():返回此线程的优先级。
  • setPriority(int newPriority):设置此线程的优先级。
  • isDaemon():判断这个线程是否是守护线程。
  • setDaemon(boolean on):将此线程标记为 daemon线程或用户线程。
  • isAlive():判断当前线程是否存活。
  • interrupt():将当前线程置为中断状态。
  • sleep(long millis):使当前运行的线程进入睡眠状态,睡眠时间至少为指定毫秒数,此时线程处于阻塞状态。
  • yield():放弃当前的CPU资源,从运行状态进入就绪状态,让其它的线程去占用CPU资源。(注:放弃的时间不确定,可能一会就会重新获得CPU资源)。
  • join():表示等待这个线程结束,即在一个线程中调用other.join(),将等待other线程结束后才继续本线程。

(2)、Object类中的方法:

wait():让当前线程进入等待阻塞状态,直到其他线程调用了此对象的notify()或notifyAll()方法后,当前线程才被唤醒进入就绪状态。

  • notify():唤醒在此对象监控器(锁)上等待的单个线程。
  • notifyAll():唤醒在此对象监控器(锁)上等待的所以线程。

注:wait()、notify()、notifyAll()都依赖于同步锁,而同步锁是对象持有的,且每个对象只有一个,所以这些方法定义在Object类中,而不是Thread类中(这三个方法非常重要,后面会详细介绍)。

(3)、yield()、sleep()、wait()比较:

  • wait():让线程从运行状态进入等待阻塞状态,并且会释放它所持有的同步锁。
  • sleep():让线程从运行状态进入阻塞状态,不会释放它所持有的同步锁。
  • yield():让线程从运行状态进入就绪状态,不会释放它所持有的同步锁。

3、线程的优先级

上面就说到过JVM的调度是根据优先级高低来抢占CPU资源的,那么我们怎么给一个线程设置优先级呢?接下来介绍:

线程的优先级分为1~~10个级别,下面的这三是Thread类中给我定义好的三个静态常量:

  • 最高优先级——MAX_PRIORITY:10
  • 最低优先级——MIN _PRIORITY:1
  • 默认优先级——NORM_PRIORITY:5

我们可以打开Thread类的源码查看到这三个常量,如下图:

image

设置优先级的方法:

  • getPriority():获取线程的优先级。
  • setPriority(int p):设置线程的优先级,参数是一个整数,范围是1~10之间,也可以使用Thread类提供的三个静态常量。

说明:高优先级的线程要抢占低优先级线程CPU的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。

简单举例:

package com.thr;

/**
 * @author Administrator
 * @date 2020-03-16
 * @desc 设置线程的优先级
 */
class MyThread implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

public class RunnableDemo {
    public static void main(String[] args) {

        MyThread m = new MyThread();

        Thread t1 = new Thread(m);
        Thread t2 = new Thread(m);

        //给两个不同的线程设置一个最高优先级,一个最低优先级,看看运行结果
        t1.setPriority(Thread.MAX_PRIORITY);
        t2.setPriority(Thread.MIN_PRIORITY);
        //t1.setPriority(9);也可以使用数字取值范围为1~~10

        t1.start();
        t2.start();
    }

}

运行结果:

image

从运行的结果可以说明,并不是优先级高的线程先执行完后,低优先级的线程才执行,只是高优先级的线程可以大概率获得CPU资源。

4、守护线程

Java中线程分为两类:用户线程(User Thread)和守护线程(Daemon Thread)。

用户线程和守护线程的区别在于:

  • 用户线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程
  • 守护线程:运行在后台,为其他前台线程服务.也可以说守护线程是JVM中非守护线程的 “佣人”。
  • 用户线程是独立存在的,不会因为其他用户线程退出而退出。
  • 守护线程是依赖于用户线程,用户线程退出了,守护线程也就会退出,典型的守护线程如垃圾回收线程。

守护线程举例:

package com.thr;

/**
 * @author Administrator
 * @date 2020-03-16
 * @desc 守护线程
 */
class MyThread implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

public class RunnableDemo {
    public static void main(String[] args) {

        MyThread m = new MyThread();
        Thread t = new Thread(m);

        t.setDaemon(true);//将t线程设置为守护线程
        t.start();

        System.out.println("main线程结束...");
        }
    }

运行结果如下:

image

唉唉唉!你看看,用户线程main方法明明都结束了,但是守护线程t还是执行了,这是为什么?是因为在main线程运行完成退出的一瞬间,守护线程获取到CPU资源,从而使守护线程执行了一小段,但是当main线程真正退出之后,这个线程也就彻底消失了。如果你要效果明显一点就在for循环中加一个sleep()即可。

守护线程注意点:

  • 如果Java没有设置线程为守护线程,那么它就是用户线程。
  • 正在运行的常规线程不能设置为守护线程。
  • thread.setDaemon(true)必须在thread.start()之前设置,否则会抛出一个IllegalThreadStateException异常。
  • 在Daemon线程中产生的新线程也是Daemon的(这里要和linux的区分,linux中守护进程fork()出来的子进程不再是守护进程)

5、线程睡眠sleep

线程睡眠就是调用sleep方法让当前正在执行的线程暂停一段时间,并进入阻塞状态。当时间到了之后就进入就绪状态而不是执行状态。下面使用代码举例:

package com.thr;

/**
 * @author Administrator
 * @date 2020-03-16
 * @desc 线程睡眠sleep
 */
class MyThread implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

public class RunnableDemo {
    public static void main(String[] args) {

        MyThread m = new MyThread();

        Thread t1 = new Thread(m);
        Thread t2 = new Thread(m);

        try {
            //注意:这里用别 t1 线程对象调用sleep方法,但是它不是睡眠 t1 线程,而是睡眠的main线程,因为sleep方法只能让当前运行的线程睡眠
            //而此时运行的正是main主线程,所以即使用其它线程对象调用sleep方法,依然是睡眠主线程。
            t1.sleep(5000);//睡眠5秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t1.start();
        t2.start();
    }
}

运行的结果会发现先等主线程睡眠了5秒,然后其它线程才开始执行。所以最好不要用线程的实例对象调用sleep方法,因为它睡眠的始终是当前正在运行的线程,而不是睡眠调用它的线程对象,它只对正在运行状态的线程对象有效。sleep方法也是一个静态方法,所以推荐使用Thread.sleep()方式调用。

6、线程让步yeild

线程让步就是调用yeild方法让当线程自己让出 CPU 资源,从运行状态进入就绪状态,让其它的线程去占用CPU资源。由于线程是进入就绪状态,所以完全有可能马上重新获取到CPU资源。这个方法开发中会很少使用该方法,该方法主要运用于调试或测试,它可能有助于多线程竞争条件下的错误重现现象。

yeild方法与sleep方法有点相似,yeild也可以让当前正在执行的线程暂停,让出cpu资源给其他的线程,只是yeild方法不能指点暂停时间的长度,并且yield方法暂停线程后直接进入就绪状态,不会进入到阻塞状态。实际上,当某个线程调用了yield()方法暂停之后,优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程更有可能获得CPU资源的机会,当然,这只是有可能,因为CPU的调度是随时的,我们干涉不了的。使用代码举例如下:

package com.thr;

/**
 * @author Administrator
 * @date 2020-03-16
 * @desc 线程让步yeild
 */
class MyThread implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 5 == 0){
                Thread.yield();//run方法中调用yeild方法
            }
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

public class RunnableDemo {
    public static void main(String[] args) {

        MyThread m = new MyThread();
        Thread t1 = new Thread(m);
        Thread t2 = new Thread(m);

        //设置线程优先级
        t1.setPriority(10);
        t2.setPriority(1);

        t1.start();
        t2.start();

        for (int i = 0; i < 100; i++) {
            System.out.println("主线程执行了:"+i+"次");
        }
    }
}

关于sleep()方法和yield()方的区别如下:

①、sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态;而yield方法调用后 ,是直接进入就绪状态,完全可能马上又被调度到运行状态。

②、sleep 方法会给其他线程运行的机会,但是不考虑其他线程优先级的问题;而yield 方法会优先给更高优先级的线程运行机会。

③、sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常。

7、线程合并join

线程合并就是调用join方法让几个并行的线程合并为一个单线程执行。join()的作用是让“主线程”等待“子线程”结束之后才能继续运行(注意这个主线程不是main线程的意思,是指当前运行的线程)。其实join不能简单的理解为合并,因为join底层实际是通过wait()来实现的,它的目的是让当前线程一直等待,然后让子线程执行。

wait()的作用是让当前线程等待进入阻塞状态。但是我们的join方法是由子线程来调用的,但是等待阻塞的却是“主线程“,而不是“子线程”!这里肯定会产生一个问题:为什么是“子线程”调用的join方法(子线程也就间接调用了wait()),那么为什么等待的不是子线程,而是主线程,这个问题后面解释。

先来看一下 join代码举例如下:

package com.thr;

/**
 * @author Administrator
 * @date 2020-03-16
 * @desc 线程合并join
 */
class MyThread implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

public class RunnableDemo {
    public static void main(String[] args) {

        MyThread m = new MyThread();

        Thread t1 = new Thread(m);
        Thread t2 = new Thread(m);

        t1.start();
        t2.start();

        //这里是main主线程的循环
        for (int i = 1; i < 10; i++) {
            System.out.println("主线程执:"+i);
            if (i == 5){
                try {
                    //当i==5时,在main主线程中调用t1、t2的join方法,让main主线程处于等待阻塞状态,等t1、t2执行完了,main主线程才有执行的机会
                    t1.join();
                    t2.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

运行结果如下图:

image

我们可以点进join()的源码查看:

image

再点入到join(long millis)方法中:

image

简单说明:从源码中,我们可以发现。当millis==0时,会进入while(isAlive())循环。即只要子线程是活的,主线程就不停的等待。

问题:son.join()被调用的地方是发生在主线程中,但是调用join()是通过子线程son。那么被调用join中对应的wait(0)也应该是让子线程son等待才对。那为什么等待的不是子线程,而是主线程呢?

回答:wait()的作用是让当前线程等待进入阻塞状态,而这里的“当前线程”是指当前在CPU上运行的线程。虽然是调用子线程的wait()方法,但是它是在“主线程”中去调用的。所以,等待的是主线程,而不是“子线程”!这一点和前面讲的sleep()方法是一样的道理,它只对正在运行状态的线程对象有效。

posted @ 2020-03-18 20:51  唐浩荣  阅读(1987)  评论(0编辑  收藏  举报