线程概念
操作系统进程的五种状态:
看完这一张图之后,我们脑子里大概有了一些了解,比如执行状态会因为一个IO请求变成阻塞。如果对进程、时间片这些概念不了解的话,可以问一下我们现在火热的chatGpt,把“什么是操作系统时间片”对chatGpt这么一问,它会告诉你答案(😂但是答案不保证官方正确且全面,有空还是建议读读操作系统权威的书籍),那么我们回归本题,Java中的线程到底是什么个概念呢?
进程 VS 线程
随着操作系统的发展,多年以前,已经从单核心发展成为多CPU、多核心的操作系统架构。整体可见如下:
那么一个进程在启动的时候是如何通过线程用到操作系统的核心的呢?
- 进程是拥有资源的最小单位;线程是可独立调度的最小单位。一个完整的进程应该包括如下,进程:一段程序 + 程序执行需要的数据 + PCB(程序控制块) 。下面通过一个简单的示例来表示一下,“进程是拥有资源的最小单位”,“线程是可独立调度的最小单位”
左图:通过顺序调用,该程序在一个线程上顺序完成;其中用到操作系统一个核心core1
右图:通过并发调用,该程序在多个线程上并行完成;其中用到了操作系统多个核心core1、core2、core3
了解完进程和线程的关系,关于进程目前可只了解 它等同于程序但不完全等于程序(进程大于一段程序),下面我们聚焦于线程-》Java线程
Java中线程的生命周期
- New(创建)
- Runnable(可运行)
- Blocked(阻塞)
- Waiting(等待)
- Timed Waiting(计时等待)
- Terminated(终止)
对比操作系统线程划分的状态,映射关系如下:
- 创建-》New(创建)
- 就绪-》Runnable(可运行)
- 阻塞-》Blocked(阻塞)、Waiting(等待)、Timed Waiting(计时等待)
- 终止-》Terminated(终止)
Runnable 可运行状态
Java中的**Runable **状态对应操作系统线程状态中的两种状态,分别是Running和Ready,也就是说,Java中处于Runnable状态的线程有可能正在执行,也有可能没有正在执行,正在等待被分配 CPU 资源。
Blocked 被阻塞状态
没有拿到锁就进入阻塞
Waiting 等待状态
- 当线程中调用了没有设置
Timeout参数的Object.wait()方法 - 当线程调用了没有设置
Timeout参数的Thread.join()方法 - 当线程调用了
LockSupport.park()方法
Timed Waiting 计时等待状态
- 最后我们来说说这个
Timed Waiting状态,它与Waiting状态非常相似,其中的区别只在于是否有时间的限制,在Timed Waiting状态时会等待超时,之后由系统唤醒,或者也可以提前被通知唤醒如notify - 但是对于 Timed Waiting 而言,它存在超时机制,也就是说如果超时时间到了那么就会系统自动直接拿到锁,或者当
join的线程执行结束/调用了LockSupport.unpark()/被中断等情况都会直接进入Runnable状态,而不会经历Blocked状态
Terminated 终止
最后我们来说最后一种状态,Terminated 终止状态,要想进入这个状态有两种可能。
- run() 方法执行完毕,线程正常退出。
- 出现一个没有捕获的异常,终止了 run() 方法,最终导致意外终止。
关于线程的一系列Q&A
终止线程的方法有哪些
- stop(); 不安全,直接终止;
- interrupt(); 线程中断并不会立即终止线程,而是通知目标线程,有人希望你终止。至于目标线程收到通知后会如何处理,则完全由目标线程自行决定。这一点很重要,如果中断后,线程立即无条件退出,那么我们又会遇到 stop() 方法的老问题。 调用thread.interrupt之后,可以采用isInterrupt()方法判断线程是否被启动了 终止。
- 自定义通过标志位退出:比如定义个修饰为volatile的变量符号你的终止条件
线程占用的大小有多大?
- jdk1.4默认的单个线程是占用256k的内存
- jdk1.5+默认的单个线程是占用1M的内存
- 可以通过-Xss参数设定,一般默认就好
- jdk1.8,每个线程占用1M内存,如果是占用的堆内存,那堆内存应该会增加190M左右,但是并不是因为线程占用堆内存,而是线程里面new了一些对象,使用了堆内存,所以线程不是占用的堆内存空间,而是堆外内存或者说是JVM结构里面的直接内存。
什么是线程的阻塞 和 线程的挂起呢?
阻塞是被动的,挂起是主动的
挂起 进程在操作系统中可以定义为暂时被淘汰出内存的进程 机器的资源是有限的,在资源不足的情况下,操作系统对在内存中的程序进行合理的安排,其中有的进程被暂时调离出内存,当条件允许的时候,会被操作系统再次调回内存,重新进入等待被执行的状态即就绪态,系统在超过一定的时间没有任何动作.
- 1)终端用户的请求。当终端用户在自己的程序运行期间发现有可疑问题时,希望暂停使自己的程序静止下来。 亦即,使正在执行的进程暂停执行;若此时用户进程正处于就绪状态而未执行,则该进程暂不接受调度,以便用户研究其执行情况或对程序进行修改。我们把这种静止状态成为“挂起状态”。
- 2)对换的需要。为了缓和内存紧张的情况,将内存中处于阻塞状态的进程换至外存上。
什么是线程中断?
- 线程中断可以按中断时线程状态分为两类,一类是运行时线程的中断,一类是阻塞或等待线程的中断。
- 有中断时,运行时的线程会在某个取消点中断执行;
- 而处于阻塞或者等待状态的线程大多会立即响应中断,比如上一篇文章中提到的join、sleep等方法,这些方法在抛出中断异常的错误后,会重置线程中断状态为未中断;
但注意,获取独占锁的阻塞状态与BIO的阻塞状态不会响应中断。而在JUC包中有在加锁阻塞的过程中响应中断的方法,比如lockInterruptibly()。
为什么要进行线程中断?
- 被动:“有时是遇到某些异常,需要中断线程。
- 主动:有时某些逻辑条件不满足 线程后续操作需要终止:有时是由于对于某种特定情况,我们知道当前线程无需继续执行下去,此时可以中断此线程;
- 主动:有时为了提高效率当前线程需要直接抢占资源,则已经运行中的线程需要响应中断才可以。可以用ReenrantLock的lockInterruptibly来响应。
具体什么目的,还要看具体场景,但线程中断的需求已经摆在那里,肯定需要。
- Java没有提供任何机制来安全地终止线程,但提供了中断机制,即thread.interrupt()方法。线程中断是一种协作式的机制,并不是说调用了中断方法之后目标线程一定会立即中断,而是发送了一个中断请求给目标线程,目标线程会自行在某个取消点中断自己。这种设定很有必要,因为如果不论线程执行到何种情况都立即响应中断的话,很容易造成某些对象状态不一致的情况出现。
Java中要如何处理线程中断?
通常的处理方式有两种,如果是业务层面的代码,则只需要做好中断线程之后的业务逻辑处理即可,
- 1.将中断异常抛出(或者在catch中重新调用interrupt()来中断线程),以告知上层方法本线程的中断经历。
- 2.使用ReenrantLock的可以使用lockInterruptibly()响应中断。
如果是在锁的获取过程中呢? 如果是阻塞的状态的线程被中断,一般通过跑出异常,不会影响它去竞争并且获取这个锁; 但是声明为lockInterruptibly可响应中断的话,一般中断了就无法获取锁。