Java面试必考题之线程的生命周期,结合源码,透彻讲解!

简介: Java面试必考题之线程的生命周期,结合源码,透彻讲解!

写在开头

在前面的几篇博客里,我们学习了Java的多线程,包括线程的作用、创建方式、重要性等,那么今天我们就要正式踏入线程,去学习更加深层次的知识点了。

第一个需要学的就是线程的生命周期,也可以将之理解为线程的几种状态,以及互相之间的切换,这几乎是Java多线程的面试必考题,每一年都有大量的同学,因为这部分内容回答不够完美而错过高薪,今天我们结合源码,好好来聊一聊。

线程的生命周期

所谓线程的生命周期,就是从它诞生到消亡的整个过程,而不同的编程语言,对线程生命周期的封装是不同的,在Java中线程整个生命周期被分为了六种状态,我们下面就来一起学习一下。

线程的6种状态

对于Java中线程的状态划分,我们其实要从两个方面去看,一是JVM层面,这是我们程序运行的核心,另一层面是操作系统层面,这是我们JVM能够运行的核心。为了更直观的分析,build哥列了一个对比图:
image.png

在操作系统层面,对于RUNNABLE状态拆分为(READY、RUNNING),那为什么在JVM层面没有分这么细致呢?

这是因为啊,在当下时分多任务操作系统架构下,线程的驱动是通过获取CPU时间片,而每个时间片的间隔非常之短(10-20ms),这就意味着一个线程在cpu上运行一次的时间在0.01秒,随后CPU执行权就会发生切换,在如此高频的切换下,JVM就没必要去区分READY和RUNNING了。

在Java的源码中也可以看到,确实只分了6种状态:

【源码解析1】

// Thread.State 源码
public enum State {
   
    //省略了每个枚举值上的注释
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

NEW(初始化状态)

我们通过new一个Thread对象,进行了初始化工作,这时的线程还没有被启动。

【代码示例1】

public class Test {
   
    public static void main(String[] args) {
   
        //lambda 表达式
        Thread thread = new Thread(() -> {
   });
        System.out.println(thread.getState());
    }
}
//执行结果:NEW

我们通过thread.getState()方法去获得当前线程所处在的状态,此时输出为NEW。

RUNNABLE(可运行状态)

对于这种状态的描述,我们来看一下Thread源码中如何说的:

/**
 * Thread state for a runnable thread. A thread in the runnable
 * state is executing in the Java virtual machine but it may
 * be waiting for other resources from the operating system
 * such as processor.
 */

大致意思是线程处于RUNNABLE状态下,代表它可能正处于运行状态,或者正在等待CPU资源的分配。

那么我们怎样从NEW状态变为RUNNABLE呢?答案很简单,我们只需要调用start()方法即可!

【代码示例2】

public class Test {
   
    public static void main(String[] args) {
   
        //lambda 表达式
        Thread thread = new Thread(() -> {
   });
        thread.start();
        System.out.println(thread.getState());
    }
}
//执行结果:RUNNABLE

BLOCKED(阻塞状态)

当线程线程进入 synchronized 方法/块或者调用 wait 后(被 notify)重新进入 synchronized 方法/块,但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞) 状态。这时候只有等到锁被另外一个线程释放,重新获取锁后,阻塞状态解除!

WAITING(无限时等待)

当通过代码将线程转为WAITING状态后,这种状态不会自动切换为其他状态,是一种无限时状态,直到整个线程接收到了外界通知,去唤醒它,才会从WAITING转为uRUNNABLE。
调用下面这 3 个方法会使线程进入等待状态:

  1. Object.wait():使当前线程处于等待状态直到另一个线程唤醒它;
  2. Thread.join():等待线程执行完毕,底层调用的是 Object 的 wait 方法;
  3. LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度。

TIMED_WAITING(有限时等待)

与WAITING相比,TIMED_WAITING是一种有限时的状态,可以通过设置等待时间,没有外界干扰的情况下,达到指定等待时间后,自动终止等待状态,转为RUNNABLE状态。
调用如下方法会使线程进入超时等待状态:

  1. Thread.sleep(long millis):使当前线程睡眠指定时间;
  2. Object.wait(long timeout):线程休眠指定时间,等待期间可以通过notify()/notifyAll()唤醒;
  3. Thread.join(long millis):等待当前线程最多执行 millis 毫秒,如果 millis 为 0,则会一直执行;
  4. LockSupport.parkNanos(long nanos): 除非获得调用许可,否则禁用当前线程进行线程调度指定时间;
  5. LockSupport.parkUntil(long deadline):同上,也是禁止线程进行调度指定时间;

TERMINATED(终止状态)

线程正常执行结束,或者异常终止,会转变到 TERMINATED 状态。

线程状态的切换

上面的6种状态随着程序的运行,代码(方法)的执行,上下文的切换,也伴随着状态的转变。
image.png

NEW 到 RUNNABLE 状态

这一种转变比较好理解,我们通过new,初始化一个Thread对象后,这时就是处于线程的NEW状态,此时线程是不会获取CPU时间片调度执行的,只有在调用了start()方法后,线程彻底创建完成,进入RUNNABLE状态,等待操作系统调度执行!这种状态是NEW -> RUNNABLE的单向转变

RUNNABLE 与 BLOCKED 的状态转变

synchronized 修饰的方法、代码块同一时刻只允许一个线程执行,其他线程只能等待,等待的线程会从 RUNNABLE 转变到 BLOCKED 状态,当等待的线程获得 synchronized 隐式锁时,就又会从 BLOCKED 转变到 RUNNABLE 状态。我们通过一段代码示例看一下:

【代码示例3】

public class Test {
   
    public static void main(String[] args) {
   
        Thread thread1 = new Thread(() -> {
   
            testMethod();
        });
        Thread thread2 = new Thread(() -> {
   
            testMethod();
        });

        thread1.start();
        thread2.start();

        System.out.println(thread1.getName()+":"+thread1.getState());
        System.out.println(thread2.getName()+":"+thread2.getState());
    }
    // 同步方法争夺锁
    private static synchronized void testMethod() {
   
        try {
   
            Thread.sleep(5000);
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        }
    }
}

输出:

Thread-0:RUNNABLE
Thread-1:BLOCKED

代码中在主线程中创建了2个线程,线程中都调用了同步方法,随后去启动线程,因为CPU的执行效率较高,还没阻塞已经完成的打印,所以大部分时间里会输出两线程均为RUNNABLE状态;

当CPU效率稍低时,就会呈现上述结果,thread1启动后进入RUNNABLE状态,并且获得了同步方法,这是thread2启动后,调用的同步方法锁已经被占用,它作为等待的线程会从 RUNNABLE 转变到 BLOCKED 状态,待到thread1同步方法执行完毕,释放synchronized锁后,thread2获得锁,从BLOCKED转为RUNNABLE状态。

RUNNABLE 与 WAITING 的状态转变

1、获得 synchronized 隐式锁的线程,调用无参数的 Object.wait() 方法,状态会从 RUNNABLE 转变到 WAITING;调用 Object.notify()、Object.notifyAll() 方法,线程可能从 WAITING 转变到 RUNNABLE 状态。

2、调用无参数的 Thread.join() 方法。join() 是一种线程同步方法,如有一线程对象 Thread t,当调用 t.join() 的时候,执行代码的线程的状态会从 RUNNABLE 转变到 WAITING,等待 thread t 执行完。当线程 t 执行完,等待它的线程会从 WAITING 状态转变到 RUNNABLE 状态。

3、调用 LockSupport.park() 方法,线程的状态会从 RUNNABLE 转变到 WAITING;调用 LockSupport.unpark(Thread thread) 可唤醒目标线程,目标线程的状态又会从 WAITING 转变为 RUNNABLE 状态。

RUNNABLE 与 TIMED_WAITING 的状态转变

这种与上面的很相似,只是在方法调用和参数上有细微差别,因为,TIMED_WAITING 和 WAITING 状态的区别,仅仅是调用的是超时参数的方法。
转变方法在上文中已经提到了,这里以sleep(time)为例,写一个测试案例:

【代码示例4】

public class Test {
   
    public static void main(String[] args) throws InterruptedException {
   

        Thread thread1 = new Thread(() -> {
   
            testMethod();
        });
        Thread thread2 = new Thread(() -> {
   
           // testMethod();
        });
        thread1.start();
        Thread.sleep(1000L);
        thread2.start();

        System.out.println(thread1.getName()+":"+thread1.getState());
        System.out.println(thread2.getName()+":"+thread2.getState());
    }
    // 同步方法争夺锁
    private static synchronized void testMethod() {
   
        try {
   
            Thread.sleep(5000);
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        }
    }
}

输出:

Thread-0:TIMED_WAITING
Thread-1:TERMINATED

这里面我们启动threa1后,让主线程休眠了1秒,这时thread1获得同步方法后,方法内部执行了休眠2秒的操作,因此它处于TIMED_WAITING状态,而thread2正常运行结束,状态处于TERMINATED(这个案例同样可以印证下面RUNNABLE到TERMINATED的转变)。

RUNNABLE 到 TERMINATED 状态

转变为TERMINATED状态,表明这个线程已经执行完毕,通常用如下几种情况:

  1. 线程执行完 run() 方法后,会自动转变到 TERMINATED 状态;
  2. 执行 run() 方法时异常抛出,也会导致线程终止;
  3. Thread类的 stop() 方法已经不建议使用。

总结

今天关于线程的6种状态就讲到这里啦,这是个重点知识点,希望大家能够铭记于心呀!

结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!

目录
相关文章
|
1天前
|
Java API
详细探究Java多线程的线程状态变化
Java多线程的线程状态主要有六种:新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、超时等待(TIMED_WAITING)和终止(TERMINATED)。线程创建后处于NEW状态,调用start()后进入RUNNABLE状态,表示准备好运行。当线程获得CPU资源,开始执行run()方法时,它处于运行状态。线程可以因等待锁或调用sleep()等方法进入BLOCKED或等待状态。线程完成任务或发生异常后,会进入TERMINATED状态。
|
1天前
|
存储 安全 Java
Java多线程中线程安全问题
Java多线程中的线程安全问题主要涉及多线程环境下对共享资源的访问可能导致的数据损坏或不一致。线程安全的核心在于确保在多线程调度顺序不确定的情况下,代码的执行结果始终正确。常见原因包括线程调度随机性、共享数据修改以及原子性问题。解决线程安全问题通常需要采用同步机制,如使用synchronized关键字或Lock接口,以确保同一时间只有一个线程能够访问特定资源,从而保持数据的一致性和正确性。
|
1天前
|
监控 安全 Java
Java多线程的使用
Java多线程允许程序同时执行多个任务,提高了系统的整体性能和响应速度。通过创建Thread类或其子类的实例,或使用Runnable接口,Java开发者可以定义并发执行的代码段。多线程在处理复杂任务、资源共享、网络通信等方面具有显著优势,但也需要注意线程安全、同步和死锁等问题。Java提供了丰富的API和工具来处理这些并发问题,使多线程编程更加高效和可靠。
|
1天前
|
API
java-多线程-CountDownLatch(闭锁) CyclicBarrier(栅栏) Semaphore(信号量)-
java-多线程-CountDownLatch(闭锁) CyclicBarrier(栅栏) Semaphore(信号量)-
6 1
|
2天前
|
存储 安全 算法
Java基础19-一文搞懂Java集合类框架,以及常见面试题(二)
Java基础19-一文搞懂Java集合类框架,以及常见面试题(二)
28 8
|
2天前
|
安全 Java 开发工具
Java基础19-一文搞懂Java集合类框架,以及常见面试题(一)
Java基础19-一文搞懂Java集合类框架,以及常见面试题(一)
23 6
|
2天前
|
安全 Java 程序员
Java基础18-一文搞懂Java多线程使用方式、实现原理以及常见面试题(二)
Java基础18-一文搞懂Java多线程使用方式、实现原理以及常见面试题(二)
23 4
|
9天前
|
存储 算法 Java
JAVA后端开发面试题库
JAVA后端开发面试题库
17 1
|
13天前
|
缓存 安全 Java
【Java面试——并发基础、并发关键字】
随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略: 先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。 乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是: 比较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。
|
22天前
|
SQL 存储 Java
致远互联java实习生面试
致远互联java实习生面试
33 0