一、线程的基本概念
1. 线程的定义
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一个进程可以拥有多个线程,每个线程执行不同的任务。
2. 线程与进程的关系
线程是进程的子集,一个进程可以包含多个线程。进程是系统进行资源分配和调度的一个独立单位,而线程是CPU调度和执行的基本单位。
3. 线程的优势
线程具有以下优势:
- 提高程序的响应速度:通过多线程技术,可以同时执行多个任务。例如:由一个线程执行及时响应的任务,而另一个线程执行耗时任务。
- 提高CPU的利用率:在单线程程序中,当程序执行I/O操作或其他等待操作时,CPU可能会处于空闲状态。多线程可以在一个线程等待时,让CPU执行另一个线程的任务,从而更充分地利用CPU资源。
- 减少内存开销:线程是进程内的执行流,它们共享进程的内存空间,这意味着线程间可以方便地共享数据,而不需要像进程间那样进行复杂的通信机制。这减少了内存的消耗,因为不需要为每个线程分配单独的内存空间。
- 简化程序结构:通过将不同的任务分配给不同的线程,可以使程序结构更加清晰,每个线程负责一个特定的功能,这样有助于代码的模块化,便于后续的维护和升级。
- 更好的资源管理:线程比进程更轻量级,创建、销毁线程的开销比进程小,因此线程在资源管理上更为高效。
二、Java中的线程
1. 线程的生命周期
Java线程的生命周期主要经历以下五个阶段:
- 新建(New):创建一个线程对象开始,到执行
start()方法启动线程之前,线程都处于新建状态。在这个状态下,线程已经有了相应的内存空间等资源,但还未被启动。 - 就绪(Runnable):执行
start()方法后,线程进入就绪状态。意味着线程已经准备好被CPU执行,等待获取CPU的执行时间。 - 运行(Running):当线程获得CPU时间片时,它就进入了运行状态,执行
run()方法中的代码。需注意,运行状态是就绪状态的一个子集,是线程实际执行代码时的状态。 - 阻塞(Blocked):线程因某些原因暂停执行,等待条件满足后恢复到就绪状态。如:
- 等待获取一个同步监视器锁(例如进入一个synchronized代码)。
- 调用
wait()方法后,等待到达最大等待时间,或另一个线程调用notify()唤醒它。 - 线程调用了
Thread.sleep()方法休眠一定时间。 - 执行I/O操作时等待I/O操作完成。
- 死亡(Terminated):线程的run()方法执行完毕或者因异常而终止后,线程进入死亡状态。一旦线程死亡,就不能再回到其他状态。
2. Thread类和Runnable接口
Java运行时的平台线程都是通过执行Thread类中的本地方法start0()创建的,在创建后会执行run()方法作为启动方法,run()方法来自于Thread类实现的Runnable接口。所以本质上Java中创建本地线程只有一种方法,就是Thread类的start0()来创建线程,创建后运行Runnable接口的run()实现方法。
3. 线程的创建与启动
通过继承Thread类创建线程:
// 继承Thread类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程运行中: " + Thread.currentThread().getName());
}
}
public class ThreadExample {
public static void main(String[] args) {
MyThread thread = new MyThread();
// 启动线程
thread.start();
}
}
通过实现Runnable接口创建线程:
// 实现Runnable接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程运行中: " + Thread.currentThread().getName());
}
}
public class RunnableExample {
public static void main(String[] args) {
// 创建Runnable对象
MyRunnable myRunnable = new MyRunnable();
// 创建Thread对象,并将Runnable对象作为参数传递
Thread thread = new Thread(myRunnable);
// 启动线程
thread.start();
}
}
使用线程池管理线程:
// 创建平台线程池
ExecutorService executorService = new ThreadPoolExecutor(
10, 10, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100)
);
// 使用线程池中线程执行任务
executorService.execute(() -> {});
// 关闭线程池
executorService.shutdown();
4. 线程的同步与通信
线程同步是为了解决多线程访问共享资源时的线程安全问题。Java提供了synchronized关键字和Lock接口实现线程同步。线程通信主要依靠Object类的wait()、notify()和notifyAll()方法。
演示代码:
// 开启同步代码,线程运行到这里时会争抢锁对象this,同一个锁对象的不同同步代码块也是同步的
synchronized (this) {
// 唤醒当前锁对象上的所有沉睡的线程
this.notifyAll();
try {
// 阻塞当前线程等待唤醒,同时释放锁
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
5. 线程池简介
在Java程序中,线程是执行任务的基本单元。但是直接创建线程存在以下这些问题:
- 创建和销毁开销:创建和销毁开销较大,频繁操作影响系统性能。
- 资源消耗:内核线程最大数量有限且占用内存,大量创建可能导致资源不足。
- 稳定性问题:过多线程竞争CPU可能会降低系统稳定性。
线程池是一种用于管理和复用线程的容器,它解决了线程频繁创建的问题,并且通过设置最大线程数量避免造成资源耗尽。线程池的工作流程大致如下:
- 提交任务到线程池
- 线程池判断核心线程是否耗尽,如果不是,则使用池中线程来执行任务;否则进入下一步
- 线程池判断任务队列是否已满,如果不是,则将任务放入队列等待执行;否则进入下一步
- 线程池判断池中的线程数是否已到达最大,如果不是,则创建一个新线程来执行任务;否则,执行饱和策略(比如拒绝任务、抛出异常等)。
相比直接创建线程,线程池:
- 降低资源消耗:重用线程减少重复创建的开销。
- 提高响应速度:省去等待线程创建时间,任务即时执行。
- 增强管理性:统一管理线程状态,避免无序创建导致资源耗尽。
- 附加功能:提供更多执行选项,如定时执行、延迟执行、线程中断等。
总之,线程池是Java并发编程的关键组件,它通过有效管理线程资源来提升程序的性能和稳定性。在Java中,java.util.concurrent包中的ExecutorService接口及其实现类,特别是ThreadPoolExecutor类,提供了线程池的核心实现。
三、虚拟线程概述
1. 什么是虚拟线程
虚拟线程是一种在用户空间(而非操作系统内核空间)中实现的线程,它们通常由程序或运行时环境(如Java虚拟机)管理,而不是由操作系统直接管理。以下是虚拟线程的一些关键特点:
- 用户态管理:虚拟线程在用户空间中创建和管理,这意味着它们的创建、调度和销毁不需要操作系统级别的上下文切换,从而减少了开销。
- 轻量级:由于不需要操作系统级别的资源分配,虚拟线程通常比平台线程(内核线程)更加轻量级。它们可以快速创建和销毁,且占用的内存更少。
- 大规模并发:虚拟线程能够支持大量的并发执行单元,不受内核线程数量限制,这对于需要高并发处理的应用程序来说非常有用。
虽然虚拟线程在用户空间中管理,但它们最终依然需要映射到内核线程以执行实际的计算工作。这种映射通常是多对一的,即多个虚拟线程映射到较少的内核线程上。
2. 虚拟线程与传统线程的区别
| 特性 | 虚拟线程 | 传统线程 |
|---|---|---|
| 运行方式 | 多对一映射到内核线程上交替执行 | 一对一映射到内核线程上执行 |
| 调度和管理 | 通常由用户空间的线程库或运行时环境管理 | 由操作系统内核直接管理,创建、调度和销毁都需要操作系统级别的操作 |
| 资源消耗 | 创建和销毁速度快,占用资源少;数量可以远大于物理线程的数量 | 每个线程由内核管理,创建和销毁代价高,线程数量受限于操作系统和硬件资源 |
| 上下文切换 | 上下文切换在用户空间内完成,不需要完整的操作系统级别上下文切换,速度更快 | 涉及操作系统级别上下文切换,包括寄存器状态保存和恢复,内存页表更新,开销较大 |
| 同步机制 | 需要特定的同步机制,不同于内核级同步原语(如互斥锁、条件变量等) | 使用操作系统提供的内核级同步机制,通常更成熟和稳定 |
| 性能和可扩展性 | 在处理大量线程时具有更好的可扩展性,管理开销较低 | 在处理少量线程时性能可能更好,尤其是线程数量接近或等于处理器核心数量时 |
3. 虚拟线程的优势
- (1)更低的资源开销;
- (2)更高的并发性能;
- (3)简化线程管理。
四、Java中的虚拟线程
1. 虚拟线程的实现原理
在Java19中,虚拟线程基于Fiber(纤程)的概念实现。它们通过用户态的线程库与操作系统进行交互,避免了直接使用操作系统线程资源,从而实现了轻量级的线程管理。这种设计允许Java应用程序在用户空间内部进行线程调度,大幅减少了线程操作的开销。
2. 虚拟线程的创建与使用
// 使用静态方法创建虚拟线程并直接运行
Thread virtualThread = Thread.startVirtualThread(() -> {
System.out.println("新线程执行...");
});
// 使用虚拟线程工厂,这样可以设置更详细的线程信息
ThreadFactory virtualThreadFactory = Thread.ofVirtual().name("myVirtual", 1000).factory();
virtualThreadFactory.newThread(() -> {
System.out.println("新线程执行...");
}).start();
3. 生命周期对比
| 生命周期阶段 | 虚拟线程 | 传统线程 |
|---|---|---|
| 创建 | 轻量级,快速 | 重量级,较慢 |
| 就绪 | 等待调度器分配执行时间 | 等待操作系统调度 |
| 运行 | 在某个内核线程上执行,可能与其他虚拟线程共享 | 独占一个内核线程 |
| 阻塞 | 挂起,释放底层内核线程 | 保持内核线程的占用,可能导致资源浪费 |
| 终止 | 轻量级销毁,快速释放资源 | 重量级销毁,可能涉及复杂的资源回收 |
五、线程与虚拟线程的应用场景
1. 传统线程适用场景
传统线程,作为操作系统级别的线程,适合处理以下场景:
- 计算密集型任务:这类任务涉及大量的计算,如复杂的数学运算、图像处理等,它们需要充分利用CPU资源。
2. 虚拟线程适用场景
虚拟线程,由于其轻量级和高效的特点,特别适用于以下场景:
- 高并发场景:在需要处理大量并发请求的环境中,如Web服务器,虚拟线程可以创建数以万计的线程而不会耗尽系统资源。
- 低延迟应用:对于需要快速响应的场景,如金融交易系统,虚拟线程可以减少线程创建和切换的开销,从而降低延迟。
- I/O密集型任务:在执行需要等待的I/O操作时,虚拟线程可以暂时释放底层内核线程,从而更加充分利用系统资源。如处理HTTP请求、维护数据库连接池等。
总结
虚拟线程以其轻量级和高效的特点,为高并发、低延迟的应用场景提供了全新的解决方案。