多线程的基本概念解析与实现方式(Java)
一、进程与线程
1. 进程
核心定义:进程是操作系统进行资源分配和调度的基本单位。
进程是操作系统中的一个执行实例,是系统进行资源分配和调度的基本单位。每个进程拥有独立的虚拟地址空间、执行环境(如打开的文件句柄、安全上下文等)和系统资源。从技术角度看,进程由程序代码、数据段、堆栈段、进程控制块(PCB)和系统资源组成。
- 独立性:每个进程拥有自己独立的虚拟内存空间,一个进程崩溃通常不会影响其他进程。这是进程最核心的优势。
- 资源开销大:创建、销毁或切换进程需要分配或回收大量资源(如内存),成本很高。
- 通信复杂:因为内存空间独立,进程间通信需要采用特定的IPC机制,如管道、消息队列、共享内存等,相对麻烦。
例子:在电脑上同时打开Chrome浏览器和Word文档,它们就是两个不同的进程。
2. 线程
核心定义:线程是进程内部的一个执行流,是CPU调度和执行的最小单位。
线程是进程内的一个执行单元,是处理器调度和执行的最小单位。线程属于创建它的进程,共享该进程的所有资源和内存空间,但拥有独立的执行栈、寄存器状态和线程控制块(TCB)。同一进程内的多个线程可以并发执行。
- 共享资源:同一进程下的所有线程共享相同的内存和资源,这使得数据共享变得非常高效和简单。
- 开销小:创建、销毁或切换线程的成本远低于进程,因为不需要分配新的内存空间等资源。
- 通信简单:由于共享内存,线程间可以直接读写同一份数据。
- 缺乏独立性:一个线程崩溃,会导致整个进程崩溃,从而影响该进程下的所有其他线程。
| 特性 | 进程 | 线程 |
|---|---|---|
| 基本单位 | 资源分配的基本单位 | CPU调度和执行的基本单位 |
| 资源开销 | 大(独立内存空间,创建/销毁/切换成本高) | 小(共享内存空间,创建/销毁/切换成本低) |
| 数据共享 | 复杂,需要IPC(进程间通信)机制 | 简单,可直接读写共享的进程数据 |
| 内存隔离 | 拥有独立的虚拟内存空间,互不干扰,安全 | 共享其所属进程的内存和资源,缺乏隔离 |
| 健壮性 | 高。一个进程崩溃通常不会影响其他进程 | 低。一个线程崩溃会导致整个进程崩溃 |
| 包含关系 | 一个进程可以包含多个线程 | 线程是进程的一部分,必须依赖于进程存在 |
3.多线程
(1).多线程的优势
提升性能(并发执行):
I/O密集型任务:当一个线程在等待网络响应或磁盘读写时(这种等待很常见),CPU可以立刻切换到另一个线程去执行计算任务,大大提高了CPU的利用率和程序的响应速度。例如,在Word中,一个线程处理你的输入,另一个线程在后台进行拼写检查。
计算密集型任务:在多核CPU上,可以将计算任务拆分给多个线程,让它们在不同的核心上真正并行执行,从而缩短总计算时间。例如,视频渲染、科学计算。
简化通信和资源共享:
线程间共享内存非常简单,无需像进程通信那样调用复杂的IPC机制。对于需要频繁交换数据的任务(如游戏引擎、数据库连接池),多线程模型更自然、高效。
更轻量:
创建和销毁线程比进程快得多,上下文切换(保存当前状态,加载下一个状态)的代价也更小。
二、并发和并行
1.并发
- 定义:并发指的是系统具有处理多个任务的能力。这些任务在时间上是重叠的,但在任意给定时刻,可能只有一个任务正在被执行。它强调的是结构设计和问题分解。
- 核心:交替执行。通过快速切换上下文,在宏观上看起来多个任务“同时”在前进,但在微观上(极短的时间片内)是串行的。
- 实现条件:并发可以在单核处理器上实现。
2. 并行
- 定义:并行指的是系统具有同时执行多个任务的能力。这些任务在同一时刻确实在不同的计算单元上一起执行。
- 核心:同时执行。它需要硬件支持,是真正的“同时发生”。
- 实现条件:并行必须在多核处理器(多CPU核心)、或多个处理器(如GPU、多路CPU)上才能实现。
进程/线程是实现并发和并行的载体。多线程是并发编程的常见模型。在单核CPU上,多个线程是并发执行的(交替)。在多核CPU上,多个线程可以并行执行(真正同时)。多进程:同样既可以用于并发,也可以用于并行,取决于硬件资源。
三、多线程的实现方式
Java 提供了丰富且不断演进的并发 API,其多线程实现方式主要可以分为以下三大类:
方式一:继承 Thread 类
这是最直接的方式,但也是最不推荐的方式,因为它限制了类的继承结构(Java 是单继承)。
步骤:
- 创建一个继承自 Thread 类的新类。
- 重写 Thread 类的 run() 方法,将需要并发执行的任务代码放入其中。
- 创建该子类的实例。
- 调用实例的 start() 方法启动线程(注意:切勿直接调用 run() 方法)。
class MyThread extends Thread {
@Override
public void run() {
// 线程要执行的任务
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
try {
Thread.sleep(500); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadExample {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
thread1.start(); // 启动第一个线程
thread2.start(); // 启动第二个线程
// thread1.run(); // 错误!这会在主线程中同步执行run(),而不是启动新线程。
}
}
方式二:实现 Runnable 接口
这是更常用和更灵活的方式。因为它允许类继承其他类,同时实现了 Runnable 接口,符合面向接口编程的原则。Thread 类本身的构造函数也接受一个 Runnable 对象。
步骤:
- 创建一个实现了
Runnable接口的类。 - 实现
run()方法。 - 创建该实现类的实例,并将其作为
Target传递给Thread类的构造函数。 - 调用
Thread实例的start()方法。
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class RunnableExample {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
// 将MyRunnable实例作为参数传递给Thread构造函数
Thread thread1 = new Thread(myRunnable, "MyThread-1"); // 可以指定线程名
Thread thread2 = new Thread(myRunnable, "MyThread-2");
thread1.start();
thread2.start();
// 使用Lambda表达式简化(Java 8+)
Runnable r = () -> {
System.out.println("Lambda thread is running");
};
new Thread(r).start();
// 更简洁的Lambda写法
new Thread(() -> System.out.println("Very concise lambda thread")).start();
}
}
优势:
- 解耦:任务(
Runnable)与执行线程的机制(Thread)分离。 - 灵活:实现类可以继承其他父类。
- 共享资源:多个线程可以更容易地共享同一个
Runnable实例的资源(但需注意线程安全!)。
方式三:实现 Callable 接口 + Future / FutureTask
Runnable 的 run() 方法没有返回值,也不能抛出受检异常。Callable 接口解决了这两个问题。
步骤:
- 创建一个实现了
Callable<V>接口的类,泛型V是返回值的类型。 - 实现
call()方法,该方法有返回值,并且可以抛出异常。 - 创建该实现类的实例。
- 使用
FutureTask<V>来包装Callable对象。 - 将
FutureTask对象作为Runnable传递给Thread构造函数并启动线程。 - 通过
FutureTask.get()方法获取线程执行后的返回值(此方法会阻塞,直到线程计算完成)。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable<Integer> {
private int count;
@Override
public Integer call() throws Exception {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
count += i;
Thread.sleep(500);
}
return count; // 返回计算结果
}
}
public class CallableExample {
public static void main(String[] args) {
MyCallable callable = new MyCallable();
// 用FutureTask包装Callable
FutureTask<Integer> futureTask = new FutureTask<>(callable);
// FutureTask实现了RunnableFuture,而RunnableFuture继承了Runnable
Thread thread = new Thread(futureTask, "CallableThread");
thread.start();
try {
// do something else in main thread...
// 获取子线程的返回值,会阻塞主线程直到计算完成
Integer result = futureTask.get();
System.out.println("Final count is: " + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
总结与最佳实践
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
继承 Thread | 编码简单 | 无法继承其他类,不够灵活 | 不推荐使用 |
实现 Runnable | 灵活,可继承其他类,可共享资源 | 无法直接获取返回值 | 实现无返回值异步任务的基础 |
实现 Callable+Future | 可获取返回值,可抛出异常 | 使用稍复杂,get()会阻塞 | 需要获取异步任务结果的场景 |
更多推荐
所有评论(0)