当前位置: 首页 > news >正文

Java EE初阶——初识多线程

1. 认识线程

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

基本概念:一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间、文件描述符等,但每个线程都有自己独立的栈空间、程序计数器和寄存器等。线程可以并发执行,从而实现进程内的多任务处理。

1. 为什么要有线程

  • 进程的缺陷

    • 创建/销毁成本高:需要分配独立的内存空间、文件描述符等资源

    • 上下文切换开销大:需要切换页表、刷新TLB(约消耗1-10μs)

    • 通信困难:必须通过IPC(管道、共享内存等)机制

  • 提高资源利用率:在没有线程的情况下,一个进程在执行时,如果遇到阻塞操作(如等待 I/O 完成),整个进程就会被阻塞,此时进程所占用的其他资源(如 CPU、内存等)就会处于闲置状态。而引入线程后,进程可以包含多个线程,当一个线程因阻塞操作而暂停时,其他线程仍可以继续执行,从而让 CPU 等资源得到更充分的利用,提高了整个系统的资源利用率。
  • 增强程序并发性:现代计算机系统通常具有多个处理器核心或支持多任务处理。通过使用线程,程序可以将不同的任务分配到不同的线程中,这些线程可以在不同的处理器核心上同时执行,实现真正的并行处理,大大提高了程序的执行效率和处理能力。即使在单处理器系统中,线程也可以通过分时复用的方式,让多个任务看似同时执行,增强了程序的并发性和响应性。
  • 优化程序结构:将一个复杂的程序分解为多个线程,每个线程负责一个特定的任务,这样可以使程序的结构更加清晰,易于理解和维护。例如,在一个图形用户界面(GUI)应用程序中,可以将界面的绘制、事件处理、数据处理等任务分别放在不同的线程中,避免因为某个任务的长时间执行而导致界面卡顿,提高用户体验。
  • 降低上下文切换成本:进程切换时需要保存和恢复整个进程的上下文信息,包括内存空间、寄存器状态等,开销较大。而线程是在同一个进程内进行切换,它们共享进程的大部分资源,只需要保存和恢复少量的线程特有信息,如程序计数器、栈指针等,因此上下文切换的成本相对较低,能够更快速地在不同任务之间进行切换,提高系统的响应速度。

总结:线程的核心价值

维度贡献值
性能释放多核算力,提升吞吐量
响应性避免I/O阻塞导致系统假死
资源利用率共享地址空间,降低内存开销
编程模型更直观地表达并发任务

线程是计算机科学中空间换时间思想的典型实践——通过消耗额外的内存和调度开销,换取更低的延迟和更高的吞吐量。

进程和线程的区别

•   进程是包含线程的: 每个进程⾄少有⼀个线程存在,即主线程
•   进程和进程之间不共享内存空间. 同⼀个进程的线程之间共享同⼀个内存空间

进程是系统分配资源的最⼩单位,线程是系统调度的最⼩单位。
⼀个进程挂了⼀般不会影响到其他进程. 但是⼀个线程挂了, 可能把同进程内的其他线程⼀起带⾛(整个进程崩溃).
Java 的线程 和 操作系统线程 的关系:
  • 底层依赖: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 线程的入口方法@Overridepublic 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{@Overridepublic 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{@Overridepublic 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(){@Overridepublic 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(){@Overridepublic 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 类是 JVM ⽤来管理线程的⼀个类,换句话说,每个线程都有⼀个唯⼀的 Thread 对象与之关联。

我们创建的为命名线程,默认是按照 Thread-0 1 2 3 4 ... 顺序命名的,为了方便调试,我们可以给不同的线程起不同的名字

Thread 的⼏个常⻅属性

后台线程(守护线程)的运行不会阻止进程结束,设为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());//mainThread 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() 方法启动线程。 

何通过覆写 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();//将中断标志设为 trueSystem.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结束后打印");}
}

相关文章:

  • 基于阿里云DataWorks的物流履约时效离线分析
  • 2025.05.07-淘天算法岗-第二题
  • UI设计公司兰亭妙微分享:汽车 MHI 设计的界面布局创新法则
  • CNG汽车加气站操作工岗位职责
  • Oracle版本、补丁及升级(12)——版本体系
  • 涨薪技术|0到1学会性能测试第56课- 堆与栈、GC回收机制
  • 深入探索 Python 的 QuTiP 5 库:量子计算与开放量子系统模拟的利器
  • Prometheus生产实战全流程详解(存储/负载/调度篇)
  • sklearn自定义pipeline的数据处理
  • stm32之USART
  • 【计算机主板架构】ATX架构
  • CN3791 锂电池充电芯片详解及电路设计要点-国产芯片
  • uniapp-商城-46-创建schema并新增到数据库
  • AI技术与园区运营的深度融合:未来生态型园区的建设路径
  • 镜头内常见的马达类型(私人笔记)
  • Python 数据分析与可视化:开启数据洞察之旅(5/10)
  • k8s之探针
  • MCP(Model Context Protocol)是专为LLM(大语言模型)应用设计的标准化协议
  • 解决 Ubuntu DNS 无法解析问题(适用于虚拟机 长期使用)
  • Spring MVC Session 属性 (@SessionAttributes) 是什么?如何使用它共享数据?
  • 春秋航空:如果供应链持续改善、油价回落到合理水平,公司补充运力的需求将会增长
  • 匈牙利外长称匈方已驱逐两名乌克兰外交官
  • 临港新片区:发布再保险、国际航运、生物医药3个领域数据出境操作指引
  • 时隔14个月北京怀柔区重启供地,北京建工以3.59亿元摘得
  • 中华人民共和国和俄罗斯联邦关于全球战略稳定的联合声明
  • 华为招聘:未与任何第三方开展过任何形式的实习合作