今天给大家分享的面试题是:一个线程调用两次start()方法会出现什么现象?这道面试题是一道关于多线程的基础面试题,很多小伙伴对这个面试题不太了解,其实,如果你看过JDK中关于Thread类的源码,那这道面试题对你来说就能过轻松应对了。
手写RPC框架视频录制中,发布地址:https://space.bilibili.com/517638832/channel/collectiondetail?sid=4186280
今天,我们就一起来聊聊这道面试题,以及面试官问这道题的面试分析拓展知识。
优质回答
Java 的线程是不允许启动两次的,第二次调用必然会抛出 IllegalThreadStateException,这是一种运行时异常,表示非法的线程状态异常。
关于线程生命周期的不同状态,在 Java 5 以后,线程状态被明确定义在其公共内部枚举类型 java.lang.Thread.State 中,分别是:
新建(NEW),表示线程被创建出来还没真正启动的状态,可以认为它是个 Java 内部状态。 就绪(RUNNABLE),表示该线程已经在 JVM 中执行,当然由于执行需要计算资源,它可能是正在运行,也可能还在等待系统分配给它 CPU 片段,在就绪队列里面排队。 在其他一些分析中,会额外区分一种状态 RUNNING,但是从 Java API 的角度,并不能表示出来。 阻塞(BLOCKED),这个状态和我们前面两讲介绍的同步非常相关,阻塞表示线程在等待 Monitor lock。比如,线程试图通过 synchronized 去获取某个锁,但是其他线程已经独占了,那么当前线程就会处于阻塞状态。 等待(WAITING),表示正在等待其他线程采取某些操作。一个常见的场景是类似生产者消费者模式,发现任务条件尚未满足,就让当前消费者线程等待(wait),另外的生产者线程去准备任务数据,然后通过类似 notify 等动作,通知消费线程可以继续工作了。Thread.join() 也会令线程进入等待状态。 计时等待(TIMED_WAIT),其进入条件和等待状态类似,但是调用的是存在超时条件的方法,比如 wait 或 join 等方法的指定超时版本,如下面示例:
public final native void wait(long timeout) throws InterruptedException;
终止(TERMINATED),不管是意外退出还是正常执行结束,线程已经完成使命,终止运行,也有人把这个状态叫作死亡。
在第二次调用 start() 方法的时候,线程可能处于终止或者其他(非 NEW)状态,但是不论如何,都是不可以再次启动的。
面试分析
今天的面试题看似简单,实则是对面试者基础知识的考察,很多大厂在面试时,很重视面试者对基础知识的掌握程度,往往这些基础知识是大家最容易忽视的。
知识拓展
线程的通用生命周期
线程在运行的过程中,会经历几种状态之间的转换,而线程在这几种状态之间的转换流程,基本上就构成了线程的生命周期。本小节,就简单介绍下线程的通用生命周期。
线程的通用生命周期总体上可以分为五种状态,分别为:初始状态、可运行状态、运行状态、休眠状态和终止状态
可以看出,线程的通用生命周期可以分为初始状态、可运行状态、运行状态、休眠状态和终止状态五种状态。
(1)初始状态:初始状态比较特殊,这种状态属于编程语言层面特有的状态,处于初始状态的线程只是线程在编程语言层面被创建了,但是在操作系统层面,并没有真正的创建线程。
(2)可运行状态:在操作系统层面,线程被真正的创建,并且可以分配CPU执行。
(3)运行状态:处于运行状态的线程已经获取到CPU资源,正在运行。
(4)休眠状态:线程正在等待某个事件的发生(例如等待I/O事件的完成),或者调用了一个阻塞的API正处于阻塞状态(例如以阻塞的方式读写文件等),此时的线程处于休眠状态。
(5)终止状态:线程正常运行结束或者出现异常,就会进入终止状态。
线程的通用生命周期中各状态之间的转换关系如下所示。
(1)初始状态转换成可运行状态:处于初始状态的线程,实际上并没有在操作系统中创建对应的线程,当在操作系统中创建了对应的线程时,此时线程就会从初始状态转换成可运行状态。
(2)可运行状态转换成运行状态:如果操作系统中存在空闲的CPU资源,则操作系统会将空闲的CPU资源分配给一个处于可运行状态的线程,处于可运行状态的线程获得CPU资源后就会转换成运行状态。也就是说,处于可运行状态的线程,被操作系统调度获取到CPU资源后,就会从可运行状态转换成运行状态。
(3)运行状态转换成可运行状态:当正在运行的线程CPU时间片用完时,就会从运行状态转换成可运行状态。
(4)运行状态转换成休眠状态:处于运行状态的线程如果等待某个事件的发生(例如,等待I/O事件的完成),或者调用了一个阻塞的API(例如,以阻塞的方式读写文件等),此时处于运行状态的线程就会释放CPU的资源,从运行状态转换成休眠状态。
(5)休眠状态转换成可运行状态:如果处于休眠状态的线程等待的事件已经发生(例如,等待的I/O事件已经完成),或者调用的阻塞API已经完成操作(例如,以阻塞的方式读写文件已经完成),则线程就会从休眠状态转换成可运行状态。
(6)运行状态转换成终止状态:处于运行状态的线程正常运行结束,或者出现异常,就会从运行状态转换成终止状态。处于终止状态的线程,不会再转换成其他的状态,线程的生命周期也就结束了。
注意:在线程的通用生命周期中,只有处于运行状态的线程可以直接转换成终止状态和休眠状态,处于其他状态的线程都不能直接转换成终止状态和休眠状态。处于休眠状态的线程只能直接转换成可运行状态,不能直接转换成其他状态。
Java中线程的生命周期
在Java中,线程的生命周期主要包括:初始化状态、可运行状态、阻塞状态、等待状态、超时等待状态和终止状态。其中,可运行状态又包括运行状态和就绪状态。
可以看出,在Java的线程生命周期中,总体上包含初始化状态、可运行状态、等待状态、超时等待状态、阻塞状态和终止状态六种状态。
(1)初始化状态:线程在Java中被创建,但是还没有调用线程对象的start()方法,也就是说,还没有创建操作系统层面对应的线程。
(2)可运行状态:Java线程生命周期中的可运行状态,包含操作系统中线程的运行状态和就绪状态。
(3)等待状态:处于等待状态的线程需要等待其他线程对当前线程进行通知或者中断等操作,从而进入下一个线程状态。
(4)超时等待状态:处于超时等待状态的线程需要在指定的时间内,等待其他线程对当前线程进行通知或者中断等操作。如果在指定的时间内,存在其他线程对当前线程进行通知或者中断等操作,则当前线程进入下一个状态。否则超过指定的时间,当前线程也会进入下一个状态。
(5)阻塞状态:处于阻塞状态的线程需要等待其他线程释放锁,或者等待进入synchronized临界区。
(6)终止状态:表示当前线程执行完毕,包括正常执行结束和异常退出。
Java的线程生命周期中的可运行状态,涵盖了运行状态和就绪状态。
(1)运行状态:对应操作系统中的运行状态。
(2)就绪状态:对应操作系统中的就绪状态。
在Java的线程生命周期中,各状态之间的转换关系如下所示。
1.初始化状态转换成可运行状态的场景
在Java层面,调用线程对象的start()方法,会在操作系统层面创建对应的线程,此时,线程的状态就会从初始化状态转换成可运行状态。
2.可运行状态与等待状态互相转换的场景一
(1)线程a调用synchronized(obj)获取到对象锁后,调用obj.wait()方法时,线程a的状态会从可运行状态转换成等待状态。
(2)在满足(1)时,此时线程b调用synchronized(obj)获取到对象锁后,调用obj.notify()方法、obj.notifyAll()方法、a.interrupt()方法,此时会有两种情况,如下所示。
l 线程a竞争锁成功,则线程a会由等待状态转换成可运行状态。
l 线程a竞争锁失败,则线程a会由等待状态转换成阻塞状态。
3.可运行状态与等待状态互相转换的场景二
(1)线程a调用线程b的join()方法时,线程a会由可运行状态转换成等待状态。
(2)在满足(1)时,线程b运行结束,或者调用了线程a的interrupt()方法,则线程a会从等待状态转换成可运行状态。
4.可运行状态与等待状态互相转换的场景三
(1)线程a调用LockSupport.park()方法时,线程a会从可运行状态转换成等待状态。
(2)在满足(1)时,其他线程调用LockSupport.unpark(a),或者调用线程a的interrupt()方法,线程a会从等待状态转换成可运行状态。
5.可运行状态与超时等待状态互相转换的场景一
(1)线程a调用synchronized(obj)获取到对象锁后,调用obj.wait(long n)方法,则线程a会从可运行状态转换成超时等待状态。
(2)在满足(1)时,线程a的等待时间超过了n毫秒,或者线程b调用synchronized(obj)获取到对象锁后,调用obj.notify()方法、obj.notifyAll()方法、a.interrupt()方法,此时会有两种情况,如下所示。
l 线程a竞争锁成功,则线程a会由超时等待状态转换成可运行状态。
l 线程a竞争锁失败,则线程a会由超时等待状态转换成阻塞状态。
6.可运行状态与超时等待状态互相转换的场景二
(1)线程a调用Thread.sleep(long n)方法,则线程a会从可运行状态转换成超时等待状态。
(2)在满足(1)时,线程a的等待时间超过n毫秒,则线程a会从超时等待状态转换成可运行状态。
7.可运行状态与超时等待状态互相转换的场景三
(1)线程a调用了线程b的join(long n)方法时,线程a会从可运行状态转换成超时等待状态。
(2)在满足(1)时,线程a的等待时间超过n毫秒,或者线程b运行结束,或者调用了线程a的interrupt()方法,线程a会从超时等待状态转换成可运行状态。
8.可运行状态与超时等待状态互相转换的场景四
(1)线程a调用Locksupport.parkNanos(long nacos)方法,或者调用LockSupport.parkUntil(long millis)方法时,线程a会从可运行状态转换成超时等待状态。
(2)在满足(1)时,其他线程调用LockSupport.unpark(a),或者调用线程a的interrupt()方法,或者线程a等待超时,则线程a会从超时等待状态转换成可运行状态。
9.可运行状态与阻塞状态互相转换的场景一
(1)线程a与线程b共同争抢同一个悲观锁,线程b争抢成功,则线程a会从可运行状态转换成阻塞状态。
(2)在满足(1)时,线程b释放锁时,线程a获取到锁,则线程a会从阻塞状态转换成可运行状态。
10.可运行状态与阻塞状态互相转换的场景二
(1)线程a调用synchronized(obj)获取对象锁时,竞争失败,则线程a会从可运行状态转换成阻塞状态。
(2)在满足(1)时,调用synchronized(obj)获取对象锁时竞争成功的线程,执行同步代码块完毕,就会唤醒所有阻塞在obj对象上的线程,这些被唤醒的线程会重新竞争,如果线程a竞争成功,则线程a会从阻塞状态转换成可运行状态。如果线程a竞争失败,则线程a继续保持阻塞状态。
11.可运行状态转换成终止状态的场景
线程a正常执行结束,或者由于某种原因异常退出,线程a就会从可运行状态转换成终止状态。
如果一个线程转换成终止状态,那么就标注着这个线程已经运行结束,不能再次转换成其他状态。
往期推荐
面试谈薪报价2w,HR非得压到1.9w,还反问他:你就差这1000块钱?后来背调时问了他前同事十几个问题,对方最后直接挂了
这里有最新前沿技术资讯、技术干货等内容
点这里 ↓↓↓ 记得 关注✔ 标星⭐ 哦