下一篇:线程生命周期与通信 · 面试:多线程面试 59 道
进程与线程
| 进程 | 线程 | |
|---|---|---|
| 定位 | OS 资源分配单位 | CPU 调度执行单位 |
| 内存 | 独立地址空间 | 共享进程堆,各自栈与 PC |
| 切换开销 | 大 | 小 |
| 崩溃影响 | 进程隔离,较健壮 | 同进程线程崩溃可能拖垮进程 |
并行 vs 并发
- 并发:单核上多任务交替执行,逻辑上“同时”
- 并行:多核上真正同时执行
- 串行:一个线程顺序执行,无竞态
多线程价值:提高多核利用率、避免单线程阻塞、便于任务拆分建模。
先搞清:创建线程到底在干什么
Java 里「创建线程」分两步,不要混在一起:
- 定义任务:这段代码要在后台跑什么(
run()里干什么) - 启动线程:让 JVM 向操作系统申请一条新执行流,并去跑上面的任务
可以记成:
任务(Runnable / Callable) + 载体(Thread / 线程池) → start / submit → 真正并发执行面试说的「四种方式」,本质是 四种把「任务」交给「线程」的组合;前三种是「自己 new Thread」,第四种是「交给线程池」。
方式一:继承 Thread
把任务写在 Thread 子类的 run() 里,再 new 出来并 start()。
public class DownloadThread extends Thread { private final String url;
public DownloadThread(String url) { this.url = url; }
@Override public void run() { System.out.println(Thread.currentThread().getName() + " 下载: " + url); // 模拟下载 }}
// 使用public class Demo1 { public static void main(String[] args) { Thread t = new DownloadThread("https://example.com/a.zip"); t.setName("download-1"); t.start(); // 必须 start,不能只 run() }}内部发生了什么(简化)
new DownloadThread():只是在堆上创建一个 Thread 对象,状态是NEW,还没有 OS 线程start():JVM 向 OS 创建原生线程,并回调当前对象的run();状态变为RUNNABLErun()执行完:线程TERMINATED
优点:写法直观,适合 Demo。
缺点:Java 单继承——类已经 extends Thread 就不能再继承业务父类;任务和线程绑死,无法交给线程池复用同一套任务类。
结论:了解即可,业务代码不推荐。
方式二:实现 Runnable(最常用的一种写法)
任务单独写成 Runnable,再 new Thread(runnable).start()。Lambda () -> { ... } 就是 Runnable 的简写。
public class DownloadTask implements Runnable { private final String url;
public DownloadTask(String url) { this.url = url; }
@Override public void run() { System.out.println(Thread.currentThread().getName() + " 下载: " + url); }}
public class Demo2 { public static void main(String[] args) { Runnable task = new DownloadTask("https://example.com/b.zip"); // 等价:Runnable task = () -> System.out.println("lambda 任务");
Thread t = new Thread(task, "download-2"); t.start(); }}和方式一的本质区别
| 继承 Thread | 实现 Runnable | |
|---|---|---|
| 任务在哪 | 子类 run() | 独立 Runnable 对象 |
| 继承位 | 占用 extends | 只占 implements,还能继承别的类 |
| 同任务多线程 | 要 new 多个 Thread 子类 | 同一个 Runnable 可包进多个 Thread(一般不这么干,但任务可复用) |
| 线程池 | 不友好 | 天然兼容 pool.execute(task) |
为什么要分离 Runnable?
线程池、定时任务、框架回调都只认 Runnable/Callable,不认你的 Thread 子类。生产里 任务定义 和 线程调度 应该分开。
方式三:Callable + FutureTask(需要返回值时)
Runnable.run() 返回 void,也不能在方法签名里抛 checked 异常。需要 结果 或 异常传递 时用 Callable。
import java.util.concurrent.Callable;import java.util.concurrent.ExecutionException;import java.util.concurrent.FutureTask;
public class SumTask implements Callable<Integer> { private final int from, to;
public SumTask(int from, int to) { this.from = from; this.to = to; }
@Override public Integer call() throws Exception { int sum = 0; for (int i = from; i <= to; i++) { sum += i; } return sum; // 有返回值 }}
public class Demo3 { public static void main(String[] args) throws ExecutionException, InterruptedException { Callable<Integer> callable = new SumTask(1, 100);
// FutureTask 既是 Future(拿结果),又是 Runnable(能交给 Thread) FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask, "sum-worker"); t.start();
// 主线程继续干别的,需要结果时再 get(会阻塞直到算完) System.out.println("主线程先做别的..."); Integer result = futureTask.get(); System.out.println("1+2+...+100 = " + result); }}三个角色别混
| 类型 | 作用 |
|---|---|
Callable<V> | 定义带返回值的任务,call() 代替 run() |
FutureTask<V> | 包装 Callable,同时实现 Runnable + Future |
Future<V> | 只关心结果:get()、cancel()、isDone() |
为什么多一层 FutureTask?
Thread 的构造函数只接受 Runnable,不认识 Callable。FutureTask 把 Callable 适配 成 Runnable,所以能 new Thread(futureTask).start()。
get() 注意:阻塞等待子线程结束;子线程里抛出的异常会在 get() 时包装成 ExecutionException 抛给主线程。
方式四:线程池(生产环境默认选项)
前三种每次 new Thread(...).start() 都会 新建一条 OS 线程,开销大,线程多了还会 OOM。线程池 复用 固定数量的工作线程,任务排队执行。
import java.util.concurrent.*;
public class Demo4 { public static void main(String[] args) throws Exception { // 固定 4 个工作线程的池子 ExecutorService pool = Executors.newFixedThreadPool(4);
// 4-1:无返回值 —— execute(Runnable),等价于方式二 pool.execute(() -> System.out.println("execute: " + Thread.currentThread().getName()));
// 4-2:有返回值 —— submit(Callable),等价于方式三 Future<Integer> future = pool.submit(() -> { Thread.sleep(500); return 42; }); System.out.println("submit 结果: " + future.get());
pool.shutdown(); // 不再接新任务,已有任务跑完后关闭 }}和裸 Thread 的对比
裸 Thread: 每个任务 → new Thread → start → 跑完销毁线程池: 任务入队 → 空闲 Worker 取出执行 → Worker 继续接下一个任务| 能力 | new Thread | 线程池 |
|---|---|---|
| 线程复用 | 否 | 是 |
| 限制并发数 | 难控制 | core/max 参数 |
| 队列排队 | 无 | 有 |
| 统一异常/命名 | 需自己写 | ThreadFactory 等 |
阿里规范建议:不要 用 Executors.newFixedThreadPool 等工厂无脑创建,生产用 ThreadPoolExecutor 显式指定队列大小和拒绝策略。细节见 线程池。
四种方式一张表
| 方式 | 核心写法 | 返回值 | 典型场景 |
|---|---|---|---|
| 1. 继承 Thread | class T extends Thread → start() | 无 | 教学 Demo |
| 2. 实现 Runnable | new Thread(runnable).start() | 无 | 异步任务、配合线程池 |
| 3. Callable + FutureTask | new Thread(new FutureTask<>(callable)).start() | 有 | 子线程计算,主线程 get() |
| 4. 线程池 | pool.execute / pool.submit | submit 有 | 线上默认 |
演进关系:2 是 1 的改进;3 在 2 上加返回值;4 在 2/3 上加复用和管控。
start() 与 run()(每种方式都要遵守)
Thread t = new Thread(() -> System.out.println("hello"));
t.run(); // ❌ 当前 main 线程里普通调用 run(),没有新线程t.start(); // ✅ JVM 启动新线程,在新线程里回调 run()start() 只能调用 一次;重复 start() 抛 IllegalStateException。
常见疑问
Q:Lambda () -> {} 属于哪一种?
属于 Runnable(方式二)。若写成 () -> 123 且交给 submit,编译器会推断为 Callable(方式三/四)。
Q:方式三能否不用 FutureTask,直接 new Thread?
不能。Thread 没有 Callable 构造函数,必须经 FutureTask 或线程池 submit 适配。
Q:方式四还算「创建线程」吗?
算。线程池在初始化或扩容时仍会 new Thread(Worker),只是 业务代码 不再每次自己 new。
上下文切换
线程数常大于 CPU 核数,时间片轮转时会保存/恢复线程上下文(寄存器、PC 等),有 CPU 开销。
一句话总结
创建线程 = 定义任务 + 启动执行。四种方式从「继承 Thread」演进到「线程池」:任务与线程分离(Runnable)、要结果用 Callable+Future、线上用线程池复用 Worker。无论哪种,都要 start()/submit(),不要直接 run()。