目录
2、直接使用线程内置的标志位 isinterruptted()
一、多线程概念(进程和线程的区别和联系):
线程 与 进程 的区别:
进程:程序的一次动态执行过程,拥有独立的内存空间和资源,是操作系统资源分配的基本单位
线程:进程内的执行单元,共享进程的资源(如内存),是CPU调度的基本单位。一个进程可包含多个线程,实现任务并发执行。
并发 与 并行:
并发:多个线程交替执行(单核 CPU 通过时间片切换)。 CPU 计算能力强大,每秒可以计算几十亿次,但实际上在某一时刻,CPU 只能执行一个指令。并发是通过 CPU 在多个任务之间快速切换来实现的。由于切换速度极快,从宏观上看,这些任务就像是在同时执行的。
并行:多个线程同时执行(多核 CPU 支持)。
1.核心区别:
特性 | 进程 | 线程 |
定义 | 操作系统资源分配的基本单位,独立运行的程序实例。 | 进程内的执行单元,共享进程资源,是CPU调度的基本单位。 |
内存空间 | 拥有独立的内存空间(堆、代码段、数据段等)。 | 共享所属进程的内存空间,但每个线程有自己的栈和寄存器。 |
独立性 | 进程间相互隔离,一个进程崩溃不影响其他进程。 | 线程间共享资源,一个线程崩溃可能导致整个进程终止。 |
创建/销毁开销 | 开销大(需分配独立资源)。 | 开销小(共享进程资源)。 |
上下文切换 | 切换速度慢(需切换内存空间、文件描述符等)。 | 切换速度快(仅切换栈、寄存器等少量资源)。 |
资源占用 | 占用内存多(独立地址空间)。 | 占用内存少(共享地址空间)。 |
2.核心联系:
1.包含关系:
线程是进程的子集,一个进程至少包含一个主线程(比如main方法),可以创建多个线程。
多个线程共享进程的资源(如堆内存、文件句柄、全局变量等)。
2.协作目标:
两者都用于实现并发执行,提高程序效率。
进程用于隔离不同任务(如浏览器多标签页),线程用于高效率利用资源(如并行计算)。
3.依赖关系:
线程必须依附于进程存在,进程终止时所有线程也会终止。
3.类比理解:
进程:像工厂,拥有独立的厂房(内存)、设备(资源)、和工人(线程)。
线程:工厂内的工人,共享厂房和设备,协作完成生产任务。
二、多线程创建的三种方式(第三种常用):
1. 继承 Thread 类:
通过继承 Thread 类,并重写 run( ) 方法,直接创建线程对象。
class MyThread extends Thread {
public void run() {
System.out.println("hello MyThread");
}
}
public class Demo1 {
public static void main(String[] args) {
Thread t = new MyThread();
//启动线程
t.start();
}
}
在这里,有几个问题:
1.此处的 run 方法有什么用呢?
run方法是线程要执行的具体任务的载体。当一个线程启动后,其核心执行逻辑就写在 run 方法中。可以理解为是“线程的入口方法”。
2. start 方法有什么用呢?
start 方法的主要作用是启动一个新线程。当调用线程对象的 start 方法时,就会自动调用线程的 run 方法,开始执行 run 方法中的代码。
调用线程:
class MyThread extends Thread {
public void run() {
while(true) {
System.out.println("hello MyThread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
//启动线程
t.start();
//主线程执行的逻辑
while(true) {
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
我们正常调用 start 方法:
可以看到结果是交替打印的,但是打印的先后顺序是不缺定的。
Thread.sleep()是一个非常实用的线程控制方法,它属于 Thread
类的静态方法,用于使当前正在执行的线程暂停执行一段指定的时间(毫秒为单位)。使用这个方法需要异常处理。
上面可以理解为,主线程到了 t.start() 方法后,新的线程与主线程(main)并行执行。各自执行对应的逻辑代码。
如果线程对象直接调用 run 方法,会怎么样呢?
class MyThread extends Thread {
public void run() {
while(true) {
System.out.println("hello MyThread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
//调用 run 方法
t.run();
//主线程执行的逻辑
while(true) {
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
可以看到,结果只有 run 方法里的打印。
当线程对象直接调用 run
方法时,不会启动新线程,而是在当前线程(即 main
线程)中执行 run
方法里的代码。需要把 run 方法执行完(run方法这里给了while死循环,所以执行不完run方法),才能执行到后面的 打印 hello main 循环。
2.实现 Runnable 接口:
实现 Runnable 接口,并重写 run 方法 ,并作为参数传递给 Thread 对象。
class MyRunnable implements Runnable {
public void run() {
System.out.println("hello Thread");
}
}
public class Demo_A {
public static void main(String[] args) {
Runnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
//启动线程
t.start();
}
}
3. 基于Lambda 表达式创建线程(推荐):
基本语法格式:
public class Demo_B {
public static void main(String[] args) {
Thread t = new Thread(() -> {
//线程执行逻辑
//......
});
//启动线程
t.start();
}
}
使用 Lambda 表达式创建线程时,不需要显式地重写 run 方法。
() -> { //线程执行逻辑 }
这个 Lambda 表达式实现了Runnable
接口的run
方法。Lambda 表达式左侧的()
表示run
方法没有参数,右侧的代码块则是run
方法的具体实现(相当于右侧的 { } 就是run方法)。
因此,虽然代码里没有像传统方式那样显式地定义一个类去实现Runnable
接口并重写run
方法,但 Lambda 表达式已经隐式地完成了run
方法的实现。
三、线程的几种属性:
在此之前,先了解什么是前台线程,后台线程:
前台线程:
新创建的线程(比如main线程和自己新创建的线程)默认是前台线程。
线程没运行完,进程就不会结束,(线程能够阻止进程结束)。
后台线程:
线程没运行完,进程可以结束,(线程不能阻止进程结束)。
//设置 thread 线程为后台线程
thread.setDaemon(true);
属性 | 类型 | 获取方法 | 说明 |
线程ID | long | getId ( ) | 唯一标识线程的ID,JVM分配,不同线程不重复。 |
线程名称 | String | getName() | 默认格式为 Thread-N (如 Thread-0 ) |
线程优先级 | int | getPriority() | 范围:1(最低)-10(最高),默认5。优先级仅作参考,操作系统可能不遵循。 |
线程状态 | Thread.State | getState() | 状态包括:NEW , RUNNABLE , BLOCKED , WAITING , TIMED_WAITING , TERMINATED 。 |
是否后台线程 | boolean | isDaemon() | 后台线程随主线程结束而终止,新创建的线程默认是前台线程。 |
是否存活 | boolean | isAlive() | 线程是否已启动且未终止(start() 后且未结束得到的是true) |
是否被中断 | boolean | isInterrupted() | 检查线程是否被中断,或主动中断线程。 |
代码示例:
public class Demo_B {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("子线程执行任务");
}, "我的线程"); // 指定线程名称
// 设置属性
//设置线程优先级
thread.setPriority(8);
//设置线程为后台线程
thread.setDaemon(true);
// 启动线程
thread.start();
// 获取属性
System.out.println("线程ID: " + thread.getId());
System.out.println("线程名称: " + thread.getName());
System.out.println("优先级: " + thread.getPriority());
System.out.println("是否后台线程: " + thread.isDaemon());
System.out.println("线程状态: " + thread.getState());
System.out.println("是否存活: " + thread.isAlive());
}
}
此处的线程状态为 TERMINATED ;表示为代表线程已经执行完毕,处于终止状态。
四、线程的属性执行过程详解:
1、启动线程:
线程通过 start 方法启动,并且一个线程对象只能调用一次 start 方法(如果调用两次会抛出 IllegalThreadStateException
异常)。
一旦线程 启动后,代码会立即往下执行,不会产生任何阻塞等待(可以粗糙理解为代码停止了一会),比如:
//启动线程
t.start();
//主线程逻辑
//......
t.start()后,代码会立即往主线程逻辑继续执行,同时并行执行两个线程代码逻辑。
2、获取线程对象引用:
可借助 Thread.currentThread()
方法获取当前正在执行的线程对象引用。该方法是 Thread
类的静态方法,无论在何处调用,都能返回当前执行代码的线程对象。例如,在主线程中调用,就返回主线程的对象;在某个子线程中调用,就返回该子线程的对象。
public class Demo_C {
public static void main(String[] args) {
Thread t = new Thread(() -> {
// 在子线程中获取当前线程对象
Thread threadName = Thread.currentThread();
});
//启动线程
t.start();
}
}
3、中断一个线程:
正常情况系下,一个线程,需要把入口方法执行完,才能使线程结束,有时候,又希望能够让这个线程提前结束(比如线程在 Thread.sleep()过程中)。
1、通过变量中断一个线程:
public class Demo_A {
public static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while(flag) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
//启动线程
t.start();
//让t线程执行 5 秒
Thread.sleep(5000);
//中断线程
flag = false;
}
}
我们定义了一个 flag 变量 ,作为 线程中while循环的“钥匙”,五秒过后,flag 置为 false ,线程也被终止。
2、直接使用线程内置的标志位 isinterruptted()
Thread 对象中,包含了一个 boolean 变量,如果为 false,说明没有人去尝试终止这个线程;如果为true,说明有人尝试终止。
public class Demo_C {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello thread");
}
});
//启动线程
t.start();
//主线程休眠 5 秒
Thread.sleep(5000);
//调用这个方法,就是把标志位 isInterrupted() 从false 改为 true
t.interrupt();
}
}
线程while循环里 Thread.currentThread() 得到该线程的对象,再通过 t.interrupt() 控制该对象标志位得到返回值决定循环的开始和结束。
有一种特殊情况,就是当线程在sleep()的时候,会触发异常:
public class Demo_C {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
//启动线程
t.start();
//主线程休眠 5 秒
Thread.sleep(5000);
//调用这个方法,就是把标志位 isInterrupted() 从false 改为 true
t.interrupt();
}
}
如果 t 线程在 sleep(),此时 main 中调用 interrupt()方法,就能把 sleep 唤醒。
sleep 提前唤醒,触发异常之后,然后 sleep 就会把标志位 isInterrupted 重置为 false。
所以,也可以理解为这种情况相当于给我们更多可操作空间。
public class Demo_C {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//throw new RuntimeException(e);
//添加break之后,此时触发异常的时候也会结束循环
//在break之前,也可以添加其他代码逻辑,相当于善后
for (int i = 3; i >= 0; i--) {
System.out.println("结束倒计时" + i);
}
break;
}
}
});
//启动线程
t.start();
//主线程休眠 5 秒
Thread.sleep(5000);
//调用这个方法,就是把标志位 isInterrupted() 从false 改为 true
t.interrupt();
}
}
所以,t 线程中进行了sleep()等阻塞操作,t 的 isInterrupted()还是会返回 true ,但是sleep()如果是被提前唤醒,抛出 InterruptException异常,同时也会把 isInterrupted()返回结果设置为 false,从而进入 catch
块执行相应的代码。此时需要手动决定是否要结束线程了。()
4、等待一个线程:
join()方法作用是阻塞当前线程,等待目标线程执行完毕。
常用于线程间顺序控制,例如主线程等待子线程完成后再处理结果。
public class Deom_D {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for (int i = 5; i > 0; i--) {
System.out.println("子线程还有" + i + "秒做完任务");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
//启动线程
t.start();
//主线程(main)等待子线程(t)
t.join();
System.out.println("主线程结束");
}
}
由于join()也是会造成线程阻塞,所以main主线程 throws InterruptedException 。
要注意的是:
哪个线程中调用 join,该线程就是“等的一方”(此处是main)。
join 前面是哪个引用,对应的线程就是“被等的一方”(此处是 t )。
join 的核心方法:
方法签名 | 说明 |
void join() | 无限期等待目标线程终止。 |
void join(long millis) | 最多等待 millis 毫秒,超时后继续执行。(等的一方的线程继续执行) |
五、线程的状态:
在上面的线程的几种属性的表格中,就有列举出,现在讲解其中部分线程状态:
状态 | 枚举值 | 说明 |
---|---|---|
新建(NEW) | NEW | 线程对象已创建,但未调用 start() 方法。 |
终止(TERMINATED) | TERMINATED | 线程已执行完毕(run() 方法结束)或异常终止。 |
可运行(RUNNABLE) | RUNNABLE | 线程已启动(start() 被调用),正在 JVM 中等待 CPU 时间片或正在执行。 |
等待(WAITING) | WAITING | 线程无限期等待其他线程的唤醒信号(如 join() )。 |
超时等待(TIMED_WAITING) | TIMED_WAITING | 线程在指定时间内等待唤醒(如 sleep(n) , join(n) )。 |
怎么查看呢?
比如有这样一段代码:
public class Deom_D {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while(true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"我的线程");
//启动线程
t.start();
//主线程(main)等待子线程(t)
t.join();
System.out.println("主线程结束");
}
}
这里 子线程的名字为 “我的线程” ,主线程名字默认为 “main”
这两个线程对应的状态应该为 超时等待(TIMED_WAITING) 和 等待(WAITING)
怎么查看?
在安装jdk文件夹目录下找到这个:
连接对应的线程后可以看到: