看看你的基础:Java线程状态之间如何转换?
面试被问:Java线程有哪些状态?状态之间如何转换?——遇到这种问题,”背“是背不完的,关键要理解。面试官可以一直追问,比如说为什么叫做RUNNABLE状态,而不是RUNNING? Thread.join后是什么状态……层出不穷。
为了解决这些问题,还是让我们从底层开始,去了解实现原理。去思考问题背后的本质,回到操作系统的角度看并发,学习必须掌握的底层理论,帮助大家从以一种更底层、更具体的视角审视并发编程。 将这类问题一网打尽。
这节课关联的主要面试题目有:
- 进程和线程的区别?
- 为什么要有线程?
- 进程开销为什么比线程大?
- Java线程是内核级线程还是用户级线程?
- JAVA线程有哪些状态?如何转换?
- 为什么要减少线程切换?
什么是进程?
每个Java程序执行起来, 是一个进程,进程(Process)是程序的执行副本。
Java是程序,和微信、QQ、Word等是类似的,被安装在操作系统中,执行起来就是进程。操作系统一开始是没有应用程序这个概念的。每次开机,程序员就加载一段程序进去,执行完就关机。那个时候没有应用程序的概念,因此也没有进程。后来一个操作系统通常会装多个应用程序,就有了进程。
进程是应用程序在内存中的执行副本。副本就是一个应用程序可以打开多个实例;比如多开QQ,这就是多个执行副本,也就是多个QQ进程。Java在一台服务器上执行多个服务,其实就是启动了多个Java的进程。
什么是线程?
线程(Thread)还被称为是轻量级的进程。在Linux设计中,现在线程还是被称为Light-Weighted-Process(LWP)。
应用越做越复杂,比如一个web服务,要接收请求、要请求数据库、要进行业务计算——这就需要并发执行任务。
并发(concurrent),是指在一段时间内多个任务看上去同时进行。所以并发并不要求任务真正同时被处理,只要在给定的一段时间,看上去任务在同时执行就是并发。所以在高并发场景,服务器需要在很小的时间内完成大量的并发工作,而并不是说每个用户的请求都同时被响应,这里面还可以使用队列。
并行(parallel),则不同,指任务被同时执行。通常并行需要用到多道设计——比如多个CPU就是多条执行指令的流水线。
并发代表任务在分时执行,或者叫做交错(interleaved)执行;并行意味着任务同时执行(simultaneously)。
下面用两张图,帮助大家区分并发和并行。
线程是一种并发的模型,每次每个线程只执行一个小的时间片段,然后切换到另一个线程执行,这样,用户感觉到的就好像任务在同时执行。
进程和线程的模型
进程是一个线程的容器,可以为每个需要并发的任务分配一个线程,多个线程共享CPU的时间去轮流切换执行,这就构成了一个最基本的线程模型。
资源分配问题
进程和线程间有个资源分配的问题。
对于程序执行,最基本的是3种资源:
- 计算资源(CPU)
- 内存
- 文件
为什么会有线程?
早期操作系统把这3个资源都分配给进程,计算资源给到进程,并由操作系统管理。应用开发者们对分时要求上升后,再把计算资源(也就是CPU)分配给进程就不是很方便了。因为进程的开发者还需要自己实现分时算法,将计算资源再次分配给独立的一个个线程。因此,操作系统开始从底层支持线程。
主线程
进程创建后,操作系统除了分配内存和文件资源外,还为进程创建一个主线程,用于执行程序。如果进程想要创建更多的线程,可以调用操作系统提供的API创建。
思考:线程创建需要什么?
创建线程需要提供一段程序,本质上是提供一段程序指令所在的内存地址,程序指令是编译器编译好,进程启动的时候加载到进程的内存中的。 内存直接分配到进程,文件也直接分配到进程,计算资源分配到线程。
线程内存权限
理论上进程拥有的所有线程可以看到进程的全部内存,也就是线程共享进程的地址空间;但是实际实现时,Java的程序能访问的内存空间是受限的,因为Java不支持直接操作内存地址,必须通过对象、对象成员等等。
内核级线程和用户级线程
操作系统的内核程序也可以看做是一个进程,它拥有最高权限,可以访问所有内存。为了保护内核的执行,操作系统将内存划分了内核空间和用户空间。
内核中执行的程序,执行在内核态。除内核外的其他应用,只能访问用户空间,执行在用户态。 用户空间、内核空间描述的是程序对内存的可见范围;用户态、内核态指的是指程序在哪个空间执行。
内核是应用程序和硬件沟通的桥梁。应用程序不断通过内核提供的系统调用请求硬件资源,这个时候内核中也需要有线程去承接这些任务。内核线程由操作系统直接调度。
这样就诞生了两个关联的概念:内核级线程和用户级线程。内核级线程由内核调度,用户级线程由应用自己调度。操作系统提供了系统API,允许进程申请一个用户级线程,并且分配一个对应的内核级线程。
好了,一个关键的面试问题来了,Java的线程是执行在内核态还是执行在用户态? 大家本能的可能会想到,Java线程肯定是执行在用户空间。假如我们使用用户级线程作为Java的线程。这意味着操作系统不知道这些线程存在。 如图:
内核可以看做一个完整的进程,内核中也有一个线程表。图中线程们是由用户空间的进程们管理的,也称作用户级线程。上图中:操作系统在调度的是JVM的主线程。所以主线程在内核中是登记在册的。 操作系统把执行时间片交给JVM主线程,再由JVM主线程去调度子线程,所以JVM必须实现调度算法。 内核调度的是JVM主线程,所以CPU时间片段实际上是分配给了JVM主线程。这样,对于一个Java应用的多个线程来说,CPU一次只能执行一个子线程——子线程之间无法并行(无法利用多核优势)。
因为有无法利用多核优势这样一个明显的缺点,因此JDK1.1之后,就不再使用这种方式。
我们再看看假如使用内核级线程设计。我们可以利用操作系统的能力,用m个内核级线程来执行n个JVM线程的程序。实际上Linux和Windows上,m:n是1:1。 在这个模型中,Java自己维护一个线程表,同时通过操作系统API创造内核级线程。操作系统帮助Java把用户级线程和内核级线程对应起来。对JVM来说,主线程和各个子线程是平行的,公平竞争CPU资源,可以利用多核优势。
线程的实际调度者是内核。对JVM来说,JVM意识不到这里有一个映射关系。比如创建了1000个Java线程,从Java程序员的视角,这里创建了1000个内核线程,由操作系统调度,可以利用所有CPU。对操作系统而言,操作系统可以选择只维护固定数量的内核线程,或者为每个Java线程创建1:1的内核级线程,来支持执行。
线程的状态
线程是一个执行单位,更轻量级。创建线程,只需要确定要执行什么程序。
线程最基本的状态有3个:
- 运行态(Running)
- 就绪态(Ready)
- 休眠态(Sleeping)。我们通常说的阻塞态(Blocking)是休眠态的一种情况。
新创建的线程会处于就绪态,也就是在排队执行。轮到自己执行后,切换到执行态。
在线程的执行过程中,如果线程请求了外部资源,比如磁盘。这个时候需要等待磁盘数据,可以考虑用编程的手段将线程切换到休眠态,这样可以节省CPU资源。休眠态的线程需要中断唤起,进入就绪态,继续排队。
线程无法从休眠态不排队,就执行,因此不可以从阻塞态切换到执行态。线程排队过程中没有任务执行,也就不可能从就绪态切换到休眠态。
JVM线程状态
JVM的线程有7中状态:
- 新建(NEW):JVM自己的概念,Java构造Thread类实例的过程。
- 运行(RUNAABLE):对应上面的执行态和就绪态。 对应两个状态,所以叫做RUNNABLE。
- 时钟等待(TIME_WATING)
- 阻塞(BLOCKED)
- 信号等待(WAITING)
- 终止(TERMINATED)
具体的转换过程如图所示:
JAVA是一个面向对象语言,线程对象需要初始化,所以增加了一个新建状态(NEW)。Java语言需要手动调用Thread的start方法,才能从NEW切换到RUNNABLE。然后,运行、就绪、休眠三态之间的关系没有变化。只不过,休眠对应了3种Java的状态,分别是阻塞(BLOCKED)、信号等待(WAITING)和时钟等待(TIME_WAITING)。
触发上面任何一种,线程都会休眠。只不过上面三种对应的场景不同:BLOCKED对应I/O等待场景,比如读取磁盘、等待锁的释放。WAITING对应信号等待的场景,是在等待其他线程的通知。TIME_WATING对应时钟等待的场景。
这里需要大家灵活掌握,不可能背下所有的函数。大家可以思考一道面试题,Thread.join是哪种模式呢? Thread.join目的是休眠当前线程,直到所有子线程完成,那么应该属于信号等待,等待其他线程的通知。再比如说读取socket文件属于哪种呢?这种属于I/O等待,应该属于BLOCKED。 最后,当一个线程没有程序要执行的时候(执行完成),会到终止态(TERMINATED)。
线程的切换
线程不拥有资源(内存、文件),因此线程切换的时候,需要完成两件事情:
- 保存当前线程的状态(CPU寄存器的值)
- 重新执行调度程序(操作系统),重新选择其他线程执行
面试可能会问,既然线程不拥有资源,那么ThreadLocal怎么理解? ThreadLocal是线程本地对象,是从语言层面实现的。相当于通过哈希表,把线程作为Key,存储了数据。
线程切换的行为我们也叫作Context Switch。主要是切换CPU的Context(上下文)。其实就是要把当前CPU所有寄存器的值,保存到内存中。等线程再次执行的时候,从内存中恢复CPU中所有寄存器的值。 下图是线程切换过程示例。
线程切换有2种场景:线程主动交出(Thread的yield方法),I/O Block等——主动触发中断。线程响应中断,被迫切换——被动触发中断。
无论如何,线程切换一定要触发中断。中断触发后,当前执行的线程就中断了。控制权回到操作系统手中。操作系统执行一小段程序保存当前寄存器的状态,然后执行调度算法选择下一个线程。下一步,操作系统在执行一段程序,为下一个要执行的线程恢复寄存器的值。最后新线程开始执行。
所以,线程切换,是一件成本不低的事情。操作系统响应中断,保存上百寄存器的值,执行调度程序,再恢复上百寄存器的值。
因此我们需要减少线程切换。
总结
这节课我们学习了进程和多线程的基本概念。这块是并发编程中和操作系统相关的知识。如果面试遇到底层问题,通常就来自于这个部分。对操作系统而言,JVM是一种应用。对Java程序来说,JVM是真实的机器。JVM作为一个进程的身份向操作系统申请资源,这里体现进程是操作系统分配资源的最小单位。JVM再以进程的身份向操作系统申请大量的线程,供使用者使用。这些线程映射到了操作系统内核级线程中,这种设计帮助操作系统直接调度Java线程。所以JVM不控制线程调度,JVM维护线程状态。
下一节课,我们将学习4.3 JVM底层:CAS和原子操作。