在现代软件开发中,多线程并发编程是构建高性能、高响应系统的核心能力。无论是 Web 服务器【消息队列、大数据处理、游戏服务器等】处理海量请求,还是大数据并行计算,亦或是移动端流畅的 UI 交互,都离不开多线程的支持,几乎无处不在。
一、基础概念
1.1 进程 vs 线程
-
进程 (Process):拥有独立内存空间、资源,进程间通信成本高(IPC)
操作系统资源分配的最小单位。比如你打开一个 Chrome 浏览器,就是一个进程。它拥有独立的内存空间和系统资源。
-
线程 (Thread):CPU 调度的最小单位,是进程内部的执行流。
一个进程可以包含多个线程,它们共享进程的内存和资源(如堆内存),但拥有独立的栈内存(栈、本地方法栈)和程序计数器(PC)。
比喻:进程好比一个“工厂”,线程就是工厂里的“工人”。工厂提供场地和原料(内存资源),工人们(线程)各自独立工作,但共享工厂的资源。
1.2 并发 vs 并行
1.2.1 并发
并发(Concurrency):任务之间的组织与协调。 - "响应性问题" - 设计问题(怎么把事拆开)
目的:提高系统的响应能力,避免阻塞,更好地管理复杂性。
并发是同时处理多个事情。它涉及构建程序以同时处理多个任务,任务可以在重叠的时间段内开始、运行和完成,但不一定在同一时刻。
指宏观上同时运行,微观上交替执行。看上去同时在跑,但可能在同一个 CPU 核心上通过时间片切换完成。
一个人接多个电话,快速在几个电话之间切换——每个电话都在推进,但任何时刻这个人只在说一个电话。
-
Thread、Runnable、Callable
-
线程池(ExecutorService)、任务队列
-
锁、同步机制(synchronized、Lock)
结构(design / composition)的: 把程序分解 / 组织成多个独立推进的执行流(可以是进程、线程、协程、任务、actor 等),让它们在逻辑时间上有重叠(overlapping in time),从而应对(deal with) 多个事情。
核心是“同时管理 / 处理多件事的能力”,不要求它们在物理上同时执行。
1.2.2 并行
并行(Parallelism):利用硬件资源提高吞吐/性能 - 吞吐量问题 - 运行问题(怎么把事跑快)。
目的:提高系统的吞吐量,加快计算速度。
并行性则指同时执行多个计算。它是同时运行两个或多个任务或计算的技术,利用计算机内的多个处理器或核心同时执行多个作。
并行性需要配备多个处理单元的硬件,其主要目标是提高系统的吞吐量和计算速度。
指真正的同时运行。只有在多核 CPU 上,多个线程才能分配到不同的核心上真正同时执行。
几个人各拿一个电话,同时打给不同的人说话,真正“一起干”。
-
多核 CPU 上多个线程真正同时运行
-
ForkJoinPool、parallelStream()、并行排序等
执行(execution / runtime)的: 多个计算任务在物理同一时刻真正同时推进(simultaneous / at the same physical instant),通常需要多个独立的执行单元(多核、多处理器、GPU、SIMD 向量单元等)。
核心是“同时做多件事”,目的是加速或提升吞吐。
1.2.3 总结
参考链接:《Concurrency vs Parallelism: Understanding the Difference》
https://www.linkedin.com/posts/alexxubyte_systemdesign-coding-interviewtips-activity-7397664946857345024-UPNU
Concurrency is about dealing with lots of things at once.
翻译:并发是关于同时应对很多事情
Parallelism is about doing lots of things at once.
翻译:并行是关于同时做很多事情。
- 并发是程序设计层面的能力:让多个独立的任务逻辑上重叠推进,以更好地应对复杂、多变的 workload;
- 并行是程序运行层面的现象:多个任务在物理同一时刻被不同执行单元同时推进,以加速计算。
二、Java 创建线程的四种方式
- 不需要结果:优先用
Runnable+ 线程池。 - 需要结果:优先用
Callable+ 线程池(submit返回Future)。 - 尽量避免直接频繁
new Thread(),线上服务基本都应该用线程池统一管理线程。
参考代码链接:
https://github.com/lzyzy1214/LearnThread.git
| 方式 | 实现方式 | 是否有返回值 | 异常处理 | 对继承的影响 | 资源利用 | 典型使用场景 |
|---|---|---|---|---|---|---|
继承 Thread类 | 继承 Thread,重写 run(),new Thread().start() | 否 | 内部捕获 | 占用唯一继承链、 不灵活 | 频繁 new Thread,性能一般 | 教学示例、 小脚本、 一次性简单任务 |
实现 Runnable接口 | 实现 Runnable,作为参数传给 Thread 或池 | 否 | 内部捕获 | 不影响继承层次(接口) | 可以和线程池搭配,较好 | 无返回值的异步任务, 多用于线程池 |
实现 Callable 接口+ Future | 实现 Callable,通过 FutureTask 或 submit 提交 | 是 | 可抛出受检异常,Future.get() 统一处理 | 接口,不影响继承 | 结合线程池效果最好 | 需要结果的异步计算、 任务并行汇总 |
线程池 ExecutorService | 通过 ExecutorService/ ThreadPoolExecutor 管理线程 | 取决于任务类型 (支持有/无返回值) | 通常在任务内部/Future 层处理 | 不相关 | 线程复用, 资源可控,性能最好 | 高并发、生产服务、 长期运行应用的首选 |
2.1 方式 1:继承 Thread 类
实现方式
class MyThread extends Thread {
@Override
public void run() {
// 线程执行体
}
}
new MyThread().start();
- 通过继承
Thread,重写run()方法。 - 通过调用
start()启动线程。
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 执行中...");
}
}
// 使用
public static void main(String[] args) {
new MyThread().start();
}
特点
- 返回值: 无法直接得到任务执行结果(只能通过共享变量、回调等方式间接获取)。
- 异常:
run()中异常不会向外抛出,只能在内部捕获处理。 - 多继承限制: Java 单继承,如果一个类已经继承了其他父类,就不能再继承
Thread。
| 类型 | 具体点 | 说明 |
|---|---|---|
| 优点 | 实现简单直观 | 对初学者友好。 |
| 语义直接 | 把“线程”与“任务”绑在一起,概念上比较直观。 | |
| 缺点 | 扩展性差 | 由于 Java 单继承特性,无法再继承其他类。 |
| 不利于复用与管理 | 任务与线程耦合,无法分离,导致代码复用性低。 | |
| 功能受限 | 不支持返回值;不利于统一调度(通常难以与线程池良好整合)。 |
适用场景
- 教学/演示,简单的小示例。
- 临时快速写一个很简单的异步任务,对结构要求不高的情况。
2.2 方式2:实现 Runnable 接口
实现方式
class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行体
}
}
// 直接使用 Thread
Thread t = new Thread(new MyRunnable());
t.start();
或使用 Lambda:
new Thread(() -> {
// 线程执行体
}).start();
class MyTask implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 执行任务...");
}
}
// 使用(推荐)
Thread t = new Thread(new MyTask(), "业务线程-1");
t.start();
特点
- 返回值: 同样无法直接返回结果(可以通过共享变量、回调等方式)。
- 异常:
run()无受检异常声明,异常需在内部捕获。 - 多继承: 不再受单继承限制,可以继承其他类的同时实现
Runnable。
| 类型 | 具体点 | 说明 |
|---|---|---|
| 优点 | 任务与线程解耦 | Runnable 仅表示“要做的事”,Thread 表示“谁来做”,职责分离清晰。 |
| 利于线程池整合 | 天然适配 ExecutorService.submit(Runnable) 等线程池接口,便于统一管理。 | |
| 结构灵活 | 避免了 Java 单继承的限制,实现类可以同时继承其他父类。 | |
| 缺点 | 无返回值能力 | 无法直接获取执行结果(需借助共享变量或回调机制变通实现)。 |
| 异常处理不直观 | 异常必须在 run() 方法内部捕获处理,无法像普通方法调用那样向外抛出。 |
适用场景
- 大部分不需要返回值的异步任务。
- 推荐用于:需要和线程池配合的任务,或需要保持类继承层次灵活的场景。
2.3 方式3: 实现 Callable + FutureTask / 线程池提交
实现方式
方式 A:Callable + FutureTask + Thread
import java.util.concurrent.*;
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 有返回值,可抛出异常
return 42;
}
}
Callable<Integer> task = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(task);
new Thread(futureTask).start();
Integer result = futureTask.get(); // 阻塞获得结果
方式 B:配合线程池(更常见)
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<Integer> future = pool.submit(() -> {
// call() 的语义
return 42;
});
Integer result = future.get();
class ComputeTask implements Callable<Long> {
@Override
public Long call() throws Exception {
long sum = 0;
for (long i = 1; i <= 100_0000L; i++) sum += i;
return sum;
}
}
// 使用
FutureTask<Long> task = new FutureTask<>(new ComputeTask());
new Thread(task, "计算线程").start();
Long result = task.get(); // 阻塞等待结果
特点
- 返回值:
Callable<V>的call()方法可以返回结果。 - 异常:
call()可以抛出受检异常,异常会封装在Future.get()抛出的ExecutionException中。 - 通常通过
Future/FutureTask来获取结果、判断是否完成、取消任务等。
优点
- 可以获取任务结果,支持返回值。
- 支持在接口层面抛出受检异常,异常处理路径清晰。
- 适合计算型任务/需要结果的异步调用。
- 和线程池配合非常自然。
缺点
- 相比
Runnable/Thread,实现和使用略复杂(多了Future/FutureTask的概念)。 - 如果大量直接
new Thread(new FutureTask(...)),依然会有频繁创建销毁线程的问题(因此最好配合线程池)。
适用场景
- 需要线程执行结果的任务(如异步计算、远程调用结果返回等)。
- 需要未来某个时刻再取结果(
Future.get())的场景。 - 异步执行 + 结果聚合(如并行计算后合并结果)。
2.4 方式4:使用线程池(ExecutorService / ThreadPoolExecutor 等)
实现方式(典型)
import java.util.concurrent.*;
ExecutorService pool = Executors.newFixedThreadPool(4);
// 提交无返回值任务(Runnable)
pool.execute(() -> {
// 任务内容
});
// 提交有返回值任务(Callable)
Future<Integer> future = pool.submit(() -> {
return 42;
});
Integer result = future.get();
// 不再使用时关闭线程池
pool.shutdown();
ExecutorService executor = new ThreadPoolExecutor(
4, 20, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(500),
new ThreadFactoryBuilder().setNameFormat("pool-task-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
executor.submit(() -> {
// 业务逻辑
});
特点
- 线程复用:线程由池统一管理,任务来了就投递到池中,由空闲线程执行。
- 同时兼容
Runnable和Callable。 - 可配置核心线程数、最大线程数、队列类型、拒绝策略等。
优点
- 性能 & 资源利用好:避免频繁创建/销毁线程的开销,减少内存和 CPU 切换开销。
- 可控性强:可以统一管理线程数量,防止“开太多线程”把系统拖垮。
- 统一管理:方便统一设置线程工厂、命名、异常处理逻辑等。
- 更适合高并发、生产环境,几乎所有中大型项目都会使用线程池而不是裸
new Thread()。
缺点
- 需要设计和配置:池大小、队列、拒绝策略等配置不当可能导致任务堆积、拒绝、OOM 等问题。
- 相比简单
new Thread(),上手成本略高。 - 需要关注池的生命周期(何时
shutdown()/shutdownNow())。
适用场景
- 高并发 / 频繁提交任务的场景(Web 服务器、RPC 服务、定时任务调度等)。
- 延迟任务、周期性任务(配合
ScheduledThreadPoolExecutor)。 - 几乎所有生产环境长期运行的服务器端程序,推荐首选线程池而不是手动创建线程。