1. 认识线程
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
基本概念:一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间、文件描述符等,但每个线程都有自己独立的栈空间、程序计数器和寄存器等。线程可以并发执行,从而实现进程内的多任务处理。
1. 为什么要有线程
进程的缺陷:
创建/销毁成本高:需要分配独立的内存空间、文件描述符等资源
上下文切换开销大:需要切换页表、刷新TLB(约消耗1-10μs)
通信困难:必须通过IPC(管道、共享内存等)机制
- 提高资源利用率:在没有线程的情况下,一个进程在执行时,如果遇到阻塞操作(如等待 I/O 完成),整个进程就会被阻塞,此时进程所占用的其他资源(如 CPU、内存等)就会处于闲置状态。而引入线程后,进程可以包含多个线程,当一个线程因阻塞操作而暂停时,其他线程仍可以继续执行,从而让 CPU 等资源得到更充分的利用,提高了整个系统的资源利用率。
- 增强程序并发性:现代计算机系统通常具有多个处理器核心或支持多任务处理。通过使用线程,程序可以将不同的任务分配到不同的线程中,这些线程可以在不同的处理器核心上同时执行,实现真正的并行处理,大大提高了程序的执行效率和处理能力。即使在单处理器系统中,线程也可以通过分时复用的方式,让多个任务看似同时执行,增强了程序的并发性和响应性。
- 优化程序结构:将一个复杂的程序分解为多个线程,每个线程负责一个特定的任务,这样可以使程序的结构更加清晰,易于理解和维护。例如,在一个图形用户界面(GUI)应用程序中,可以将界面的绘制、事件处理、数据处理等任务分别放在不同的线程中,避免因为某个任务的长时间执行而导致界面卡顿,提高用户体验。
- 降低上下文切换成本:进程切换时需要保存和恢复整个进程的上下文信息,包括内存空间、寄存器状态等,开销较大。而线程是在同一个进程内进行切换,它们共享进程的大部分资源,只需要保存和恢复少量的线程特有信息,如程序计数器、栈指针等,因此上下文切换的成本相对较低,能够更快速地在不同任务之间进行切换,提高系统的响应速度。
总结:线程的核心价值
维度 | 贡献值 |
---|---|
性能 | 释放多核算力,提升吞吐量 |
响应性 | 避免I/O阻塞导致系统假死 |
资源利用率 | 共享地址空间,降低内存开销 |
编程模型 | 更直观地表达并发任务 |
线程是计算机科学中空间换时间思想的典型实践——通过消耗额外的内存和调度开销,换取更低的延迟和更高的吞吐量。
进程和线程的区别
• 进程是包含线程的: 每个进程⾄少有⼀个线程存在,即主线程• 进程和进程之间不共享内存空间. 同⼀个进程的线程之间共享同⼀个内存空间• 进程是系统分配资源的最⼩单位,线程是系统调度的最⼩单位。• ⼀个进程挂了⼀般不会影响到其他进程. 但是⼀个线程挂了, 可能把同进程内的其他线程⼀起带⾛(整个进程崩溃).
- 底层依赖:Java 线程是基于操作系统线程实现的。Java 虚拟机(JVM)在创建 Java 线程时,实际上是向操作系统请求创建一个对应的操作系统线程。操作系统负责为线程分配 CPU 时间片、管理线程的生命周期以及提供线程调度等功能。Java 线程的执行最终依赖于操作系统线程在底层硬件上的运行。
2. 创建线程
1. 继承 Thread 类,重写run
java标准库中有一个特殊包 java.lang 不需要手动导入
run 方法类似 main 方法,main 方法是一个java进程(程序)的入口方法
一般把 “跑起来” 的程序,称为 ”进程“,没有运行起来的程序称为 “可执行文件”
run 方法不需要手动调用,在线程创建好了之后,被 jvm 自动调用
//1. 创建一个类继承 Thread
class MyThread1 extends Thread{
//run 线程的入口方法
@Override
public void run(){
System.out.println("Thread");
}
}
public class ThreadDomo1 {
public static void main(String[] args) {
//2. 创建线程实例
Thread t = new MyThread1();//向上转型
//3. 调用Thread 的start方法,会调用系统api,在系统内核中创建出线程
//线程就可以执行上面写好的run方法
t.start();
}
}
每个线程都是一个独立的执行流
class MyThread2 extends Thread{
@Override
public void run(){
while(true){
System.out.println("Thread");
//不能加上throws,如果加throws,修改了方法签名,此时就无法构成重写
//父类的run没有throws异常,子类重写时就不能throws异常
try {
//sleep 是Thread提供的静态方法
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDomo2 {
public static void main(String[] args) {
Thread t = new MyThread2();
t.start();
while(true){
System.out.println("main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
调用start,创建线程之后
我们发现 main 先被打印,Thread 后被打印,因为主线程在调用 start 方法之后,就立即往下执行,与此同时内核就要通过刚才的线程api构建出线程,并执行run,由于创建线程本身有开销(比创建进程低),第一轮打印,在创建线程开销的影响下,导致 main 先被打印,Thread 后被打印
t 线程和 main 线程中的两个死循环轮流被执行(并发),因此,这两个线程就是两个独立的执行流
多线程执行先后顺序是不确定的,因为操作系统内核中,有一个 ”调度器“ 模块,这个模块的实现方式,类似于 “随机调度”
随机调度:当有多个进程处于就绪状态等待 CPU 资源时,随机调度算法会以随机的方式挑选一个进程,让它获得 CPU 并开始执行,不依据任务的优先级、执行时间、等待时间等传统因素来决定调度顺序。(抢占式执行)
被调度执行的线程什么时间从cpu上下来也是不确定的
打开jdk中的 bin 中的 jconsole 工具,查看多线程执行情况
其余线程都是 jvm 自带线程
2. 实现 Runnable 接⼝,重写run
class MyThread3 implements Runnable{
@Override
public void run() {
System.out.println("Runnable");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class ThreadDomo3 {
public static void main(String[] args) {
//解耦合
//上述类的实例
Runnable runnable = new MyThread3();
//搭配Thread类,才能真正在系统中创建出线程
Thread t = new Thread(runnable);
t.start();
}
}
3. 继承 Thread 类,重写 run,使用匿名内部类
Thread t = new Thread(){
@Override
public void run(){
while(true){
System.out.println("Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
4. 实现 Runnable,重写 run,使用匿名内部类
Thread t = new Thread(new Runnable(){
@Override
public void run(){
while(true){
System.out.println("Runnable");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
5. 【常用】使用 lambda 表达式
Thread t = new Thread(()->{
while(true){
System.out.println("Runnable");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
3. Thread 类及常⻅⽅法
我们创建的为命名线程,默认是按照 Thread-0 1 2 3 4 ... 顺序命名的,为了方便调试,我们可以给不同的线程起不同的名字
后台线程(守护线程)的运行不会阻止进程结束,设为true
前台线程的运行会阻止进程结束,我们创建的线程默认是前台线程,只要前台线程没执行完毕,进程就不会结束,即使 main 已经执行完毕
java 代码定义的 线程对象(Thread)实例,虽表示一个线程,但这个对象的生命周期和内核中pcb的生命周期是不完全一样的
创建Thread 实例,此时 t 对象被创建,但 内核 pcb 还未被创建,此时 isAlive() 为false,t.start(); 创建了内核 pcb 此时 isAlive() 为 true,当线程 run 执行完毕,内核中的线程(pcb)结束,但 t 变量可能仍然存在,此时 isAlive() 为false。
public class ThreadDomo7 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("Runnable");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"这是一个线程");
//在start之前将线程设置为后台线程
//t.setDaemon(true);
System.out.println("start之前: "+t.isAlive());
t.start();
System.out.println(t.getId());
System.out.println(t.getName());
System.out.println(t.getState());
System.out.println(t.getPriority());
System.out.println("start之后: "+t.isAlive());
Thread.sleep(3000);
System.out.println("t结束之后:"+t.isAlive());
}
}
4. 线程休眠 - sleep()
Thread.sleep()
是 Java 中用于暂停当前线程执行的核心方法,它可以让线程进入定时等待(TIMED_WAITING)状态,在此期间线程会释放 CPU 资源,但不会释放已获取的锁(如 synchronized
锁)。时间到期后,线程会重新进入就绪状态,等待 CPU 调度
方法 | 说明 |
---|---|
public static native void sleep(long millis) |
暂停当前线程执行指定的毫秒数。 |
public static void sleep(long millis, int nanos) |
暂停当前线程执行指定的毫秒数和纳秒数(更精确的控制,但实际精度取决于操作系统) |
异常处理
sleep()
方法会抛出 InterruptedException
异常,因此必须在代码中进行处理:
- 捕获异常:使用
try-catch
块捕获异常。 - 声明抛出:在方法签名中使用
throws InterruptedException
声明抛出。
5. 获取当前线程引用 - currentThread()
- 获取当前线程实例:
Thread.currentThread()
方法会返回一个Thread
对象,这个对象代表的就是当前正在执行这段代码的线程。 - 操作当前线程:通过返回的
Thread
对象,我们可以获取线程的各种信息,如线程 ID、线程名称、线程优先级等,也可以对线程进行操作,如设置线程优先级、中断线程等。
public class ThreadDomo12 {
public static void main(String[] args){
// 在主线程中获取线程信息
Thread t = Thread.currentThread();
//获取并打印线程名称
System.out.println(t.getName());//main
Thread t1 = new Thread(()->{
// 在子线程中获取线程信息
Thread t2 = Thread.currentThread();
//获取并打印线程名称
System.out.println(t2.getName());//Thread-0/child1/child2/child3
},"child1");
t1.setName("child2");//修改线程名称
t1.start();
t1.setName("child3");
}
}
6. 启动线程 - start()
通过创建一个继承自 Thread
类的子类,并重写 run()
方法来定义线程要执行的任务,然后创建该子类的实例并调用 start()
方法启动线程。
start 和 run 的区别:
1. 直接调用 run() 方法,,它只是在当前线程中执行
run()
方法里的代码,不会创建新线程。2. 调用
start()
方法,会触发 JVM 创建新线程并使其进入就绪状态,等待系统调度。3. 一个
Thread
对象的start()
方法只能调用一次。若多次调用start()
方法,会抛出IllegalThreadStateException
异常,因为一个线程只能被启动一次。
7. 中断线程
让 run 方法(入口方法)执行完毕
1. 使⽤⾃定义的变量来作为标志位
public class ThreadDomo9 {
public static boolean isQuit = false;
public static void main(String[] args) {
Thread t = new Thread(()->{
while (!isQuit){
System.out.println("线程进行中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("线程执行完毕");
});
t.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//System.out.println("线程中断");
isQuit = true;
System.out.println("线程中断");//与"线程执行完毕"打印先后顺序不定
}
}
2. 调⽤ interrupt() ⽅法
使用 Thread 实例内部自带的标志位
获取当前线程实例(t),哪个线程调用,得到的就是哪个线程的实例(类似于 this)
isInterrupted()
方法用于检测当前线程的中断标志。若中断标志为true
,则表明线程已被中断;若为false
,则表示未被中断。
interrupt()
方法用于向线程t
发送中断信号。调用此方法后,线程t
的中断标志会被设为true
。- 若线程
t
正处于阻塞状态(像调用Thread.sleep()
、Object.wait()
、Thread.join()
等)时,如果被其他线程调用interrupt()
方法中断,就会抛出InterruptedException
异常,同时 JVM 会自动清除该线程的中断标志,即中断标志被设置为false
。 - 此时 "线程进行中" 就会一直被打印,可以在 catch 中加上 break ,抛出异常后,线程结束。
清除标志位,是为了有更多的“可操作空间”
可以在 catch 语句中加一些代码:
1. 让线程立即结束(break)
2. 让线程不结束,继续执行
3. 让线程执行一些代码后,再结束
public class ThreadDomo10 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while (!Thread.currentThread().isInterrupted()){
System.out.println("线程进行中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
break;//结束线程
}
}
System.out.println("线程执行完毕");
});
t.start();
Thread.sleep(3000);
t.interrupt();//将中断标志设为 true
System.out.println("线程中断");
}
}
8. 等待线程 - join()
多个线程的执行顺序是不确定的(随机调度,抢占式执行)
虽然线程底层的调度是无序的,但可以在应用程序中,通过一些 api 来影响线程执行顺序
join() 方法
- 创建一个子线程
t
并启动它。 - 主线程调用
t.join()
方法,此时主线程会被阻塞,直到子线程执行完毕。 - 子线程执行完成后,主线程会从阻塞中恢复过来继续执行后续代码。
import java.util.Random;
public class ThreadDomo11 {
public static void main(String[] args) {
Thread t = new Thread(()->{
Random r = new Random();
int n = r.nextInt()%7;
for(int i=0;i<n;i++){
System.out.println("线程执行中");
}
System.out.println("线程执行完毕");
});
t.start();
try {
t.join();//线程等待,main线程等待t线程结束之后
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("主线程,期望这个日志在t结束后打印");
}
}