文章目录
- 认识进程
- 进程的概念
- 进程的特性
- 进程的状态
- 进程的优先级
- 进程的描述
- 进程的调度
- 并发与并行
- 认识线程
- 线程的概念
- Java中线程的表示
- 认识Thread类
- 线程的创建
- 线程的控制和管理
- 线程启动-start()
- 线程休眠-sleep()
- 线程中断-interrupt()
- 线程插队-join()
- 线程让步-yield()
- 线程的状态
认识进程
进程的概念
进程 是操作系统中一个执行中的程序的实例,它不仅包含程序本身,还包括程序运行时所需的资源,如内存、文件句柄、网络连接等。例如,当我们在电脑上打开qq时,操作系统就会为qq创建一个进程。
进程是操作系统进行资源分配的基本(最小)单位。 进程拥有自己的内存空间、文件句柄、环境变量等系统资源。因此进程间相互独立、互不干扰,但这不绝对,进程间可以通信,需要通过进程间通信机制(IPC)来实现。
进程与程序:
特征 程序 进程 定义 存储在磁盘上的指令集合 程序在内存中的执行实例 性质 静态 动态 资源 无独立资源 有自己的资源(内存、文件描述等) 执行 不能直接执行 可以直接执行 状态 无状态 多种状态(就绪、运行、阻塞) 并发性 无并发性 有并发性 一句话总结:程序是存储在磁盘上的静态指令集合,而进程是程序在内存中的动态执行实例。
进程的特性
进程主要有以下几个特性,这对理解进程十分重要:
- 独立性:每个进程都有自己的地址空间和其他资源,它们相互独立
- 并发性:多个进程可以同时存在于系统中,并且可以并发执行。提高了系统效率和响应速度。
- 动态性:进程是一个动态概念,有创建、执行、阻塞、唤醒、终止等生命周期状态。
- 交互性:进程之间可以通过多种方式进行通信(IPC),如管道、消息队列、共享内存等。
进程的状态
进程的状态分为:创建状态、就绪状态、运行状态、阻塞状态 以及 终止状态。
- 创建状态:进程正在被创建,即操作系统正在为其分配资源
- 就绪状态:进程已经创建完毕,准备好运行,但是还在等待操作系统调度执行。通常是在等待CPU时间片
- 运行状态:进程正在CPU上执行
- 阻塞状态:进程因为某些原因(如等待I/O操作)而暂时停止执行,当满足某些条件时才能重新进入就绪状态
- 终止状态:进程执行完成或者异常终止,操作系统回收其占用的资源
进程的优先级
进程的优先级是指操作系统根据某种策略为每个进程分配的一个数值,用于决定调度顺序。
高优先级的进程通常会比低优先级的进程更早获得CPU资源。优先级可以是静态的也可以是动态的:
- 静态优先级:在创建进程时确定,之后不再改变
- 动态优先级:根据进程的运行情况实时调整
进程的优先级作为优先级调度算法(以优先级为标准进行进程调度的算法) 的重要依据,进程的优先级调度算法主要有两种基本类型:抢占式优先级调度 和 非抢占式优先级调度,另外,还有两种调度算法:动态优先级调度 和 多级反馈队列调度,可以将它们视为上两种基本类型的具体实现或拓展形式。
-
非抢占式优先级调度:一旦一个进程开始执行,它将一直运行直到完成或自愿放弃CPU
- 调度过程:从就绪队列中选择优先级最高的进程,将其调度到CPU上执行,当此进程执行完毕或者自愿放弃CPU资源时再次从就绪队列中选择优先级最高的进程
- 优点:实现简单,每个进程都有执行的机会,不会出现饥饿现象(前提是优先级相同的进程数量有限)
- 缺点:灵活性差,高优先级的进程可能需要等待低优先级的进程,不可抢占性可能导致系统响应变慢
-
抢占式优先级调度:如果一个高优先级的进程进入就绪队列,当前正在运行的低优先级进程会被抢占,CPU资源会被分配给高优先级的进程
- 调度过程:就绪队列中选择优先级最高的进程,将其调度到CPU上执行,如果有更高优先级的进程进入就绪队列,当前进程会被抢占,CPU资源会被分配给高优先级的进程
- 优点:灵活性高,高优先级的进程可以优先获得CPU资源,提高了系统的响应速度
- 缺点:频繁的上下文切换会增加系统开销,容易出现饥饿现象,即存在大量的高优先级的进程,使得低优先级的进程长时间得不到CPU资源
-
动态优先级调度:根据进程的执行情况动态调整优先级,以优化系统性能
-
调整策略:
时间片老化:随着时间的推移,进程的优先级会逐渐降低
I/O惩罚:频繁进行I/O操作的进程优先级降低
CPU饥饿:长时间没有获得CPU资源的进程优先级提高
-
优点:减少了饥饿现象,提高了系统的整体性能
-
缺点:实现复杂,增加系统开销
-
-
多级反馈队列调度:将进程分为多个优先级队列,每个队列有自己的时间片
- 调度过程:进程首先被放入高优先级的队列,如果进程在当前的队列的时间片内没有完成,它会被转移到下一个优先级较低的队列中,如果进程在低优先级队列中等待一段时间后,它可以被提升到较高优先级的队列
- 优点:减少了饥饿现象;结合多种调度算法的优点,适用于不同类型的进程
- 缺点:实现复杂,多级队列管理增加系统开销
总结:非抢占式优先级调度简单但响应慢;抢占式优先级调度响应快但上下文切换频繁;动态优先级调度和多级反馈队列调度结合了多种策略,提高了系统的整体性能和公平性但开销较大。
进程的描述
进程主要由三部分组成:程序代码、数据 以及 进程控制块(PCB)。
程序代码即进程要执行的指令集;数据包括全局变量、静态变量、堆栈等;进程控制块是用来描述进程的信息结构。
操作系统采用PCB(Process Control Block,进程控制块) 结构体来描述进程。PCB 是操作系统管理进程的核心数据结构,它包含了描述和控制进程所需要的所有信息。
PCB结构体中的一些关键属性:
- PID:进程的唯一标识符,用于唯一标识一个进程
- 内存指针:一组指针,其中包含指向进程的虚拟地址空间的数据结构(如页表)的指针。操作系统通过内存指针可以找到要执行的指令以及指令依赖的数据。(进程的运行需要消耗内存资源)
- 文件描述符表:记录了打开的文件和设备。有助于操作系统追踪哪些资源被哪些进程占用。这里涉及到“一切皆文件”的设计理念,即操作系统会将很多的资源抽象为文件来管理,包括管道、套接字等。(进程的运行需要消耗硬盘、网卡等资源设备)
- 进程的状态:描述创建、就绪、运行、阻塞、终止状态。
- 进程的优先级:表示进程的优先级,用于优先级调度算法。
- 进程的上下文:即进程在执行过程中所需的所有状态信息。当操作系统调度进程进行切换时,必须记录当前进程的上下文,当再次轮到该进程执行时,就要根据记录的上下文信息从上一次执行的位置接着执行。如果没有上下文信息,进程可能就要从头开始执行了,这肯定是不可以的。
- 进程的记账信息:指与进程资源使用和执行情况相关的统计信息。根据这些记录的信息,可以进行系统管理,例如某个进程占用CPU时间过长,就要考虑让该进程“停一停”了。
进程的调度
进程的调度涉及几个概念:进程调度、就绪队列、运行队列、阻塞队列、进程调度算法。
进程调度:操作系统根据一定的策略,从就绪队列中选择一个进程,让其在CPU上运行。
就绪队列:包含所有准备就绪但是还未获得CPU资源的进程
运行队列:包含所有正在运行的进程
阻塞队列:包含所有因等待某些事件而暂时无法运行的进程
进程调度算法:进程调度算法是操作系统的一个核心组件,它决定了哪个进程何时在CPU上运行。进程调度算法按不同的分类依据有很多种分类方式,主要有下面三种分类方式:
-
按是否抢占
-
非抢占式调度
-
定义:一旦一个进程开始执行,它将一直运行直到该进程执行完毕或者主动放弃CPU资源
-
特点:实现简单但灵活性较差
-
常见算法:非抢占式优先级调度,先来先服务
-
-
抢占式调度
- 定义:当前运行的进程在某些条件下可能被其他进程抢占
- 灵活性较高但上下文切换频繁,切换开销大
- 常见算法:抢占式优先级调度、时间片轮转
-
-
按调度策略(标准)
-
基于时间的调度
- 定义:根据时间相关参数进行调度
- 常见算法:时间片轮转
-
基于优先级的调度
- 定义:基于进程的优先级进行调度
- 常见算法:抢占式优先级调度
-
基于响应比的调度
- 定义:基于进程的响应比进行调度
- 常见算法:高响应比优先
-
基于执行时间的调度
- 定义:基于进程的执行时间进行调度
- 常见算法:短作业优先
-
-
按调度层次
- 单级调度
- 定义:只有一个调度队列,所有进程都在同一个队列中竞争CPU资源
- 特点:实现简单、较为公平
- 常见算法:时间片轮转、短作业优先
- 多级调度
- 定义:进程分为多个队列,每个队列有自己的调度算法
- 特点:灵活性高但是实现复杂
- 常见算法:多级反馈队列调整
- 单级调度
以上对进程的基本介绍简单理解记忆即可,更加重点的知识是接下来的并发与并行的介绍以及多线程。
并发与并行
并发:多个任务在一段时间内交替执行,共享系统资源,但由于交替(切换)间隔很短,容易给人一种同时进行的错觉。
例如:笔记本上运行的游戏和聊天软件,它们其实不是真正同时运行的,而通常是并发执行的,只是切换太快,我们无法察觉。
并行:指多个任务在同一时刻真正同时执行,通常依赖于多核处理器或多台计算机。
例如:现代计算机都是多核心,操作系统可以将不同的任务分配给不同的核心,实现真正的同时执行。
我们日常生活中通常只会说并发,如并发编程,但其实本质上这个日常所说的并发包含了并发和并行,只是将它混用了而已。因此,如果没有特殊说明,并发就是并发+并行的统称。
现代计算机系统中,不论是线程还是进程,实际上都同时存在并发执行和并行执行。 这也是为什么我们习惯将两者混为一谈而不做特别区分。
认识线程
线程的概念
线程 是一种轻量级进程,是CPU调度的最小单位。
那么,为什么会出现线程的概念呢?
前面提到,线程是一种轻量级进程,当创建进程时,操作系统会为其分配资源,当销毁进程时,操作系统又会释放掉其占有的资源,这就导致如果进程频繁创建和销毁,开销会很大。基于这一问题,线程的概念应运而生,线程是基于进程创建的,即一个进程中可以存在多个线程,这些线程共享这同一个进程的某些资源,如CPU资源、网络资源、内存资源,但是每个线程又有自己独有的一些资源,如寄存器状态、栈等。每次创建线程时,只需使用已经分配好的进程的资源即可,而不需要操作系统额外进行资源分配,而当销毁线程时,也不需要操作系统进行资源的回收,只有当进程中的所有(前台)线程销毁完毕时,操作系统才会将此进程占有的资源回收,因此线程的创建销毁开销远远少于进程的创建销毁开销。
一句话总结:线程是为了解决进程频繁创建销毁开销大的问题而出现的。 但是线程也不是越多越好,当线程数量过多时,多线程的效果会逐渐变差,因为线程调度的开销会很大,同时发生问题(如:死锁问题)的概率也会增加。
Java中线程的表示
认识Thread类
Java中的线程用 Thread
类的对象表示,Thread
类是JVM用来管理线程的一个类,换句话说,每个线程都有唯一的Thread
对象与之关联。后续介绍的创建线程的所有方法,本质上都离不开Thread
类,因此我们要先了解Thread
。
常见构造方法
构造方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象并命名 |
Thread(Runnable target, String name) | 使用Runnable 对象创建线程对象并命名 |
Thread(ThreadGroup group, Runnable target) | 使用Runnable 对象创建线程对象,并将其添加到指定的线程组中 |
常见属性
属性 | 含义 | 获取方法 |
---|---|---|
ID(tid ) | 线程的唯一标识,不同线程不会重复 | getId() |
名称(name ) | 线程的名字,可以重复,调试时会用到 | getName() |
状态(threadStatus ) | 表示线程当前的状态,具体有哪些状态可以跳转到子目录线程的状态 | getState() |
优先级(priority ) | 优先级高的线程理论上来讲更容易被调度到 | getPriority() |
线程组(group ) | 表示线程所属的线程组 | getThreadGroup() |
是否后台线程(daemon ) | 后台线程又叫守护线程,它不决定JVM是否退出。与之对应的是前台线程(非守护线程),当所有前台线程都结束时,JVM就会退出,不会等待守护线程执行完毕 | isDaemon() |
是否存活 | 表示线程是否仍在运行 | isAlive() |
是否被中断(interrupted ) | 表示线程是否已被中断 | isInterrupted() |
-
Java的
Thread
类中,并没有一个直接公开的布尔属性来标识线程是否存活,但是提供了isAlive()
方法来检查是否存活 -
上面提到的某些属性的值可以由我们自己设置,例如:
-
void setName(String name)
:设置线程名称 -
void setPriority(int priority)
:设置线程的优先级该方法的参数要求一个
int
类型的值,这个范围是从1~10的,默认值是5,其中Java提供了几个可读性好常量也可以作为参数:Thread.MIN_PRIORITY
:1Thread.NORM_PRIORITY
:5Thread.MAX_PRIORITY
:10
但如果传入无效的优先级(大于10或小于1),该方法会自动将优先级调整到最近的有效值,具体来说:如果传入的值小于1,优先级会被设置为1;如果传入的值大于10,优先级会被设置为10。
如此设置优先级只是一个建议,具体的执行和调度是由操作系统决定的。Java虚拟机(JVM)会尽量遵循这个优先级设置,但最终的调度行为还是依赖于操作系统的调度策略。
-
void setDaemon(boolean on)
:设置守护线程
-
-
关于守护线程,我们只需要记住一句话:只有一个进程的所有非后台线程结束后,才会结束运行。
-
线程是否被中断不等价于线程是否存活,一个线程被中断后,仍然可以被调度继续运行,而一个线程一旦终止(不存活),就不能再被调度运行。具体的内容会在后续补充。
线程的创建
严格来说,Java中创建线程的方式只有一种:
Thread.start();
,而创建线程体的方式有很多种,包括但不限于:
- 继承
Thread
类- 实现
Runnable
接口- 线程池
- 实现
Callable
接口但是我们平常所说的“创建线程的方法”其实指的是创建线程体的方法,我们要有这一点认识。同时,上面提到的内容后续都会介绍到。但是,
Thread
对象的生命周期和对应操作系统线程的生命周期并不是相同的。
tip:大多数现代JVM实现中,Java中创建的线程和操作系统的线程是一一对应的。
Java中线程的创建方式有很多种,具体涉及的组件包括但不限于Thread
类、Runnable
接口、Callable
接口。此处我们的讨论主要围绕Thread
类和Runnable
接口。
Thread
类 和 Runnable
接口都在java.lang
包中,Runnable
用于表示一个任务,具体的任务就是其中的run()
方法,run()
方法体就是线程要执行的任务逻辑,Thread
类底层也实现了Runnable
接口,可以基于它们创建线程,具体做法:
-
继承
Thread
类具体:自定义一个类继承
Thread
类并重写run()
方法,然后使用Thread
类的引用 引用自定义类的对象(向上转型) -
实现
Runnable
接口具体:自定义一个类实现
Thread
类并重写run()
方法,使用实现类的对象构造Thread
对象
可以采用匿名内部类简化上述操作,同时,由于
Runnable
是一个函数式接口,因此可以进一步使用Lambda表达式化简。后续我们常用的就是实现Runnable
接口并使用Lambda表达式的形式,这种方式使得创建线程 与 要执行的任务区分开来,能更好的解耦合。
示例代码1:
//继承Thread类
class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("继承Thread类的方式创建线程体");
}
}
}
//实现Runnable接口
class MyThread2 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("实现Runnable接口的方式创建线程体");
}
}
}
public class Demo17 {
public static void main(String[] args) {
Thread t1 = new MyThread1();//使用Thread类的引用 引用自定义类的对象
Thread t2 = new Thread(new MyThread2());//使用实现类的对象构造Thread对象
}
}
示例代码2:
public class Demo20 {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("匿名内部类");
}
});
Thread t2 = new Thread(() -> System.out.println("Lambda表达式"));
t1.start();
t2.start();
}
}
上面只是基于两种方式创建了两个线程体,当执行时并不会创建线程,如果想实际创建出线程,需要调用start()
方法,start()
方法会自动在新线程调用执行run()
方法。展示完善后的代码,并演示执行结果:
//继承Thread类
class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("继承Thread类的方式创建线程体");
}
}
}
//实现Runnable接口
class MyThread2 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("实现Runnable接口的方式创建线程体");
}
}
}
public class Demo17 {
public static void main(String[] args) {
Thread t1 = new MyThread1();//使用Thread类的引用 引用自定义类的对象
Thread t2 = new Thread(new MyThread2());//使用实现类的对象构造Thread对象
//创建两个线程,执行run()任务
t1.start();
t2.start();
}
}
执行结果显示,两个线程都创建并执行了各自线程体中run()
方法的任务。同时发现,两次执行的打印结果不同,这就是前面提到的并发执行,多线程并发执行,就会出现多个线程的切换以及调度,线程的调度是“随机”的,进而出现上示现象。
【JConsole】
需要注意的是,上述代码执行过程中,实际上创建了三个线程,分别是main线程、t1线程、t2线程。这一现象可以使用 JConsole 工具查看,JConsole是JDK的一部分,能够帮助我们观察线程信息,通常位于bin目录下:
为了方便展示,我们将主线程、t1线程、t2线程执行的任务修改为一个死循环使得三个线程存活,方便观察:
- 如图所示,存在我们提到的三个线程:main线程、t1线程、t2线程。除此之外,还有很多其他的线程,都属于后台线程,这些线程负责执行各种系统级任务,确保 JVM 的正常运行。
- 这里引入了JConsole工具辅助线程学习,后续我们会经常用到。
正如上面提到的,Java中创建线程的方法有很多,大部分都会在后续展开介绍,这里实际上只介绍了两种:
- 继承
Thread
类- 实现
Runnable
接口
线程的控制和管理
线程的控制和管理部分将进一步介绍Thread
类中的一些常用方法,这些方法用来操作干预线程,但是这些干预是有一定限制的,就像设置线程的优先级一样。
线程启动-start()
start()
是一个实例方法,由具体的线程对象调用。只有调用start()
方法,才真正在操作系统的底层创建出一个线程。
简单的例子理解start()
前后的含义:
- 重写
run
方法相当于分配任务 - 线程对象的创建相当于将张三、李四叫过来了
- 调用
start
方法相当于招呼大伙开始行动完成任务
区分 通过线程对象调用
start()
和 直接调用run()
:
- 通过线程对象调用
start()
:创建启动一个新的线程,使run()
方法在新的线程的上下文中执行。- 直接调用
run()
:直接在当前线程中执行run()
方法,不会创建新的线程,与调用普通方法没有区别。
注意:一个Thread
对象只能调用一次start()
方法! 这是Java 规范明确规定的,避免多次调用 start()
方法可能导致的线程管理的混乱。
因此,如果我们想实际创建并启动一个线程,一定要调用start()
方法。
线程休眠-sleep()
线程休眠是指当前正在执行的线程暂时停止执行一段时间,以便让出 CPU 给其他线程使用。Java中的线程休眠是通过Thread
类中的静态方法sleep
,sleep
方法有两个版本:
方法 | 说明 |
---|---|
static native void sleep(long millis) | 线程休眠millis 毫秒 |
static native void sleep(long millis, int nanos) | 线程休眠millis 毫秒以及额外的nanos 纳秒 |
【代码演示】
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
int i = 0;
while(i <= 5) {
if(i == 3) {
try {
System.out.println("t1休眠3s");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1: " + i);
i++;
}
});
t1.start();
int i = 0;
while(i <= 5) {
System.out.println("main: " + i);
i++;
}
}
注意事项
- 休眠方法会使线程从
RUNNABLE
状态变为TIMED_WAITING
状态(处在该状态的线程不会被调度运行),当休眠时间结束,线程会再次被唤醒。注意:被唤醒的线程不会立即执行,而是进入了就绪状态(RUNNABLE
状态进一步划分的READY
状态)等待调度器的再次调度。 - 如果当前线程在休眠期间被中断,
Thread.sleep
方法会抛出InterruptedException
- 实际的休眠时间可能会因为操作系统调度和其他因素的影响产生轻微的偏差,大于等于设置值。
Thread.sleep
不会释放任何锁(抱着锁睡觉)。如果当前线程在同步代码块或方法中调用Thread.sleep
,它将继续持有锁,即使在睡眠期间。
面试题:
Thread.sleep(0)
有意义吗?
Thread.sleep()
方法是Java线程调度的一部分,它让当前运行的线程暂停执行并进入休眠状态,让出CPU的执行权,这个方法的底层是调用操作系统的sleep
或者nanosleep
系统调用。Thread.sleep(0)
这个调用虽然没有传递休眠时长,但还是会触发线程调度的切换,即:当前线程会从运行状态变为就绪状态(准确来说是从运行状态变为计时等待状态然后转变为就绪状态,但中间一个状态的切换是很快的),然后操作系统的调度器再根据优先级来选择一个线程来执行,如果有优先级更高的线程正在等待CPU的时间片,那么这个线程就会得到执行;如果没有,那么可能就会再次选择刚刚进入就绪状态的这个线程来执行,具体的调度策略取决于操作系统层面的调度算法。
进一步提问:线程调度算法有哪些呢?
线程具体的调度算法思想与进程的差不多,可以依据上面介绍的进程调度算法进行理解回答。
线程中断-interrupt()
在Java中,线程中断是一个协作机制,用于通知线程应该立即停止执行。 其对应的方法是:Thread.interrupt()
,当线程调用该方法时,线程不会被强制停止;相反,它是一个礼貌性的请求,告诉目标线程应该尽快停下来,线程可以选择如何响应这个中断请求或者完全忽略。具体讲:当线程调用interrupt()
方法时,线程的interrupted
属性的值会被设置为true
,仅此而已。
与线程中断相关的方法有三个:
方法 | 说明 |
---|---|
void interrupt() | 设置线程的中断状态为true |
static boolean interrupted() | 检查当前线程的中断状态,并在检查后清除该状态,方法返回一个boolean 类型的值。 |
boolean isInterrupted() | 检查当前线程的中断状态,但不清除该状态,方法返回一个boolean 类型的值。 |
-
Thread.interrupted()
:通常用在需要处理中断后,不希望保留中断标志的场景。Thread.isInterrupted()
:适用于循环检查中断状态的情况,因为它不会影响中断标志位。 -
两个检查方法用于响应中断(实现中断策略),一种具体做法是不断检查中断状态,根据中断状态的变化实现某些行为。具体使用哪个取决于业务场景需求,看是否需要消除中断状态。但
Thread.isInterrupted()
方法更常用于检查线程的中断状态,因为它允许多次检查中断状态而不影响其他逻辑。 -
线程中断的另一个典型的场景(特性)就是配合
sleep()
、wait()
或join()
等方法使用,这几个方法都会使得线程被挂起,如果在挂起时调用interrupt()
方法,线程会被提前唤醒并抛出InterruptedException
,此时可以通过catch
语句捕获异常并做出一些行为,比如提前结束任务等。当
interrupt()
提前唤醒挂起方法,如sleep()
,必然会抛出异常,捕获的同时也会清除线程的中断标志位,将isInterrupted()
标志位设置为false
。此时如果catch
语句中没有重新设置中断标志位,isInterrupted()
方法将返回false
。(这一点在后面的演示中还会出现)
【代码演示】
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(!Thread.currentThread().isInterrupted()) {
System.out.println("循环中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("提前被唤醒,退出循环");
break;
}
}
System.out.println(Thread.currentThread().isInterrupted());
System.out.println("t1线程结束");
});
t1.start();
Thread.sleep(3000);
t1.interrupt();
}
-
t1 线程的线程体中
while
循环括号中不能写!t1.isInterrupted()
,因为此时Thread
对象还没有创建出来(意味着 t1 还没有被赋值)。Thread.currentThread()
用于返回对当前正在执行的线程对象的引用,以这种方式书写就没有问题。 -
t1 线程的
while
循环中的打印语句执行极快,这就意味着当 main 线程调用 t1 线程的interrupt()
方法时,t1 线程大概率正在休眠,此时中断方法就会提前唤醒线程并抛出异常并被捕获,接着执行catch
代码块中的语句。 -
当捕获
InterruptedException
异常时(挂起线程被提前唤醒),Java的线程库会自动 清除(即设置为false)线程的中断状态标志。这就是为什么在打印Thread.currentThread().isInterrupted()
时看到的是false
,而不是true
。如果希望恢复中断状态,则调用再一次中断方法即可。此时,如果将
catch
代码块清空,即执行到catch
语句不会有break
逻辑,此时while
循环将继续执行,因为标记位被清除为false
了,满足循环条件。public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { while(!Thread.currentThread().isInterrupted()) { System.out.println("循环中"); try { Thread.sleep(1000); } catch (InterruptedException e) { //空 } } System.out.println(Thread.currentThread().isInterrupted()); System.out.println("t1线程结束"); }); t1.start(); Thread.sleep(3000); t1.interrupt(); }
一直处在
while
循环中,这种特性给了程序员更大的选择空间。可以看到,线程中断
interrupt()
方法并不是强制的。在Java中其实有一个强制终止线程的方法stop()
,但是它已经被废弃了,因为这种方式极不安全,导致数据不一致等不可预测的后果。
线程插队-join()
join()
方法用于等待一个线程终止。当你调用一个线程的join()
方法时,当前线程会暂停执行,直到被调用join()
方法的线程终止。 这个方法通常用于确保某个线程在继续执行之前已经完成其任务。这个过程就像插队,因此可以称为线程插队。
join()
方法有三个版本:
方法 | 说明 |
---|---|
void join() | 等待被调用线程终止 |
void join(long millis) | 等待被调用线程终止,最多等待millis 毫秒 |
void join(long millis, int nanos) | 等待被调用线程终止,最多等待millis 毫秒和nanos 纳秒 |
- 前面提到,
join()
方法也是会被提前唤醒的,并抛出InterruptedException
。因此,处理join()
方法时应该考虑中断情况 join()
能做到让一个线程在某个线程之前执行,与sleep()
相比会更加严谨,它会确保万无一失,而sleep()
不能join()
方法有带参数版本,指定了最大等待时间,避免了死等,更加灵活合理。假如最大等待时间设置为3000ms
,插队线程在这个时间内就结束了,被插队的线程不会等满3000ms
;如果插队线程在最大等待时间内没有结束,被插队的线程会退出等待状态,不再等待插队线程结束。- 如果一个线程已经终止,再次调用它的
join()
方法不会有任何效果,不会抛出异常,也不会阻塞调用线程
【代码演示】
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for(int i = 0; i < 3; i++) {
System.out.println("hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t线程结束");
});
t.start();
t.join();
System.out.println("main线程结束");
}
- 在 main 线程中出现了
t.join()
,意味着被调用的 t 线程会在当前的 main 线程之前执行。通俗来讲,谁.join()
谁插队,插的谁的队,这个语句在哪个线程就插的哪个线程的队。
我们可以将 t 线程设置为死循环,然后插到 main 线程之前,通过JConsole观察main线程是否阻塞在join()
方法上:
- JConsole显示,main线程阻塞在第15行,即
t.join()
语句。
线程让步-yield()
Thread.yield()
方法用于当前线程让出CPU时间片,让其他具有相同优先级的线程有机会运行。yield()
方法是一个静态方法,调用它时,当前线程会暂时让出CPU,但并不保证其他线程一定会立即运行,因为线程调度器可能会忽略这个请求。
方法 | 说明 |
---|---|
static native void yield() | 提示当前线程愿意让出其当前使用的CPU,调度器可以自由地忽略此提示 |
yield()
不保证当前线程会立即停止运行,特别是如果当前线程是唯一可运行的线程时 或者 当前线程的优先级高于其他线程时,线程调度器可能会忽略yield()
请求。因此,yield()
方法主要影响具有相同优先级的线程。
【代码演示】
该方法不常用,且不一定达到预期效果,因此演示代码较为简单:
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Thread 1: " + i);
Thread.yield();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Thread 2: " + i);
Thread.yield();
}
});
t1.start();
t2.start();
}
- 可以看到,在这个例子的这次运行中,
yield()
方法被调度器忽视了。
线程的状态
在Java中,线程的状态是通过 Thread.State
枚举来表示的。每个线程在其生命周期中会经历不同的状态,如图展示了线程的状态以及会促使状态相互切换的一些手段:
我们可以通过如下代码打印所有的状态:
for(Thread.State state : Thread.State.values()) {
System.out.println(state);
}
-
NEW
:线程体(任务)被创建但尚未启动。 -
RUNNABLE
:可工作的,线程正在JVM中执行,但可能正在等待操作系统资源,即:线程要么在运行,要么准备好运行。 -
BLOCKED
:线程被阻塞,等待监视器锁以进入同步块或方法。当多个线程竞争同一个锁时,只有一个线程可以获得锁并继续执行,其他线程会被阻塞。 -
WAITING
:线程在等待另一个线程执行某个特定的动作,没有时间限制,即:如果没有收到结束通知,会无休止的等待。 -
TIMED_WAITING
:线程在等待另一个线程执行某个特定的动作,但有一个时间限制。 -
TERMINATED
:线程已经结束执行,要么是正常返回,要么是因为抛出了未捕获的异常。
以上状态都可以使用getState()
方法 或者 JConsole
观察。
【观察NEW
】
public static void main(String[] args) {
Thread t = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
});
System.out.println(t.getState());
}
【观察RUNNABLE
】
public static void main(String[] args) {
Thread t = new Thread(() -> {
while(true) {
System.out.println(100);
}
});
t.start();
}
运行后通过JConsole
观察:
【观察BLOCKED
】
BLOCKED
状态与锁相关,具体内容后续介绍,这里仅给出状态演示。
public static void main(String[] args) throws InterruptedException {
final Object lock = new Object();
Thread t1 = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("Thread 1 is running");
Thread.sleep(5000); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Thread 1 is interrupted");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println("Thread 2 is running");
}
});
t1.start();
Thread.sleep(100); // 确保t1先获取锁
t2.start();
// 等待一段时间,确保t2进入BLOCKED状态
Thread.sleep(100);
// 打印t2的状态
System.out.println("Thread 2 state: " + t2.getState());
// 等待t1和t2完成
t1.join();
t2.join();
}
【观察WAITING
和TIMED_WAITING
】
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while(true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
t.join();
}
观察 main 线程(join
引起):
观察 t 线程(sleep
引起):
线程状态的意义更多的是帮助我们调试程序,找bug,总之大家好好理解记住,以备不时之需!
完