创建线程
本教程共 100 篇 · 第 94 篇 · 更新于 2026-08-05 · 约 14 分钟阅读
94. 创建线程
本节目标:掌握创建线程的四种方式,彻底搞懂 start() 和 run() 的区别。
方式一:继承 Thread
最直观的做法是继承 Thread 类,重写 run() 方法:
public class MyThread extends Thread {
private final String task;
public MyThread(String task) {
this.task = task;
}
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println(getName() + " 执行 " + task + " 第 " + i + " 次");
}
}
public static void main(String[] args) {
MyThread t1 = new MyThread("下载");
MyThread t2 = new MyThread("上传");
t1.start(); // 启动线程,不是调用 run()
t2.start();
System.out.println("main 线程结束");
}
}
```text
某次运行的输出:
```text
main 线程结束
Thread-0 执行 下载 第 1 次
Thread-1 执行 上传 第 1 次
Thread-0 执行 下载 第 2 次
Thread-1 执行 上传 第 2 次
Thread-1 执行 上传 第 3 次
Thread-0 执行 下载 第 3 次
注意:你的输出多半和上面不一样。三个线程谁先跑、跑多少完全由操作系统调度决定,每次运行结果都可能变。
这种不确定性是并发编程的常态,写代码时不能依赖任何执行顺序。
start 和 run 的区别
这是并发部分的第一个必考点。
start() 是「启动一个新线程」。JVM 会向操作系统申请创建线程,然后由那个新线程去执行 run() 方法体。
run() 只是「调用一个普通方法」。谁调用它,就在谁的线程里执行,压根不会产生新线程。
MyThread t = new MyThread("测试");
t.run(); // 错误示范:在 main 线程里同步执行,没有并发
t.start(); // 正确:真正开启新线程
```bash
> [!WARNING]
> 一个 `Thread` 对象的 `start()` 只能调用一次。第二次调用会抛 `IllegalThreadStateException`。线程结束后无法重启,想再跑一遍就得新建对象。
## 方式二:实现 Runnable
继承 `Thread` 有个硬伤:Java 是单继承,你的类继承了 `Thread` 就没法继承别的类了。
更常用的做法是实现 `Runnable` 接口:
```java
public class DownloadTask implements Runnable {
private final String file;
public DownloadTask(String file) {
this.file = file;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()
+ " 正在下载 " + file);
}
public static void main(String[] args) {
Runnable task1 = new DownloadTask("a.zip");
Runnable task2 = new DownloadTask("b.zip");
// Runnable 只是任务,需要交给 Thread 才能跑
new Thread(task1, "线程A").start();
new Thread(task2, "线程B").start();
}
}
输出:
线程A 正在下载 a.zip
线程B 正在下载 b.zip
```bash
`Runnable` 的优势不只是绕开单继承限制,更重要的是它把「任务」和「执行任务的线程」拆开了。
任务只描述「要做什么」,至于用哪个线程跑、要不要放进线程池,由调用方决定。这种职责分离让代码灵活得多。
`Thread` 构造方法的第二个参数可以指定线程名。给线程起个有意义的名字,出问题看日志时能省一大截时间。
## 方式三:Lambda 简写
`Runnable` 是函数式接口——只有一个抽象方法。所以可以直接用 Lambda:
```java
public class LambdaThread {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("Lambda 里的线程:" +
Thread.currentThread().getName());
}, "工作线程");
t.start();
// 单行任务还能更短
new Thread(() -> System.out.println("再来一个"), "临时线程").start();
}
}
任务逻辑简单时,这种写法最清爽,不用为几行代码单独定义一个类。
sleep 与 join
两个常用的控制方法。
Thread.sleep(毫秒) 让当前线程暂停一段时间。它是静态方法,写在哪个线程里就影响哪个线程。
t.join() 让调用它的线程等待,直到线程 t 执行完毕。
public class JoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
for (int i = 1; i <= 3; i++) {
System.out.println("工作中 " + i);
try {
Thread.sleep(200); // 暂停 200 毫秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
System.out.println("工作完成");
});
worker.start();
System.out.println("main 等待 worker...");
worker.join(); // main 线程在这里卡住,直到 worker 结束
System.out.println("main 继续执行");
}
}
```text
输出(这次顺序是确定的):
```text
main 等待 worker...
工作中 1
工作中 2
工作中 3
工作完成
main 继续执行
join() 是把并发拉回顺序执行的手段。需要等所有子任务算完再汇总结果时,就得用它。
sleep() 会抛受检异常 InterruptedException,必须处理。捕获后调用 Thread.currentThread().interrupt() 恢复中断标志,是标准做法。
Tip
Thread.sleep(Duration.ofSeconds(2))是 Java 19 加入的重载,比数零更不容易出错。
方式四:Callable 与 Future
前面三种方式有个共同缺陷:run() 返回 void,拿不到计算结果,也没法把异常抛给调用方。
Callable<V> 解决了这个问题。它的 call() 方法有返回值,还能抛受检异常:
import java.util.concurrent.*;
public class CallableDemo {
public static void main(String[] args) throws Exception {
Callable<Integer> task = () -> {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
Thread.sleep(300); // 模拟耗时
return sum;
};
ExecutorService pool = Executors.newSingleThreadExecutor();
Future<Integer> future = pool.submit(task);
System.out.println("任务已提交,main 可以先干别的");
System.out.println("是否完成:" + future.isDone());
Integer result = future.get(); // 阻塞等待结果
System.out.println("1 到 100 求和 = " + result);
pool.shutdown();
}
}
```text
输出:
```text
任务已提交,main 可以先干别的
是否完成:false
1 到 100 求和 = 5050
Future 代表「一个将来才会有的结果」。提交任务后立刻返回,可以先做别的事,需要结果时再调 get()。
get() 会阻塞到任务完成。还有个带超时的版本 get(long timeout, TimeUnit unit),超时就抛 TimeoutException,避免无限期等下去。
Callable 不能直接交给 Thread,必须通过线程池提交。线程池的细节留到第 100 节。
四种方式怎么选
| 方式 | 适用场景 |
|---|---|
继承 Thread | 需要重写 Thread 其他方法时,实际很少用 |
实现 Runnable | 任务逻辑较复杂、需要复用时 |
| Lambda | 任务只有几行,用完就扔 |
Callable + Future | 需要返回值或需要抛异常 |
一句话总结:优先 Runnable 或 Lambda,需要结果就用 Callable,别去继承 Thread。
线程里的异常不会传出来
有个坑必须提前知道:子线程抛出的异常,主线程捕获不到。
public class ExceptionInThread {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
throw new RuntimeException("子线程炸了");
}, "问题线程");
t.setUncaughtExceptionHandler((thread, e) ->
System.out.println("捕获到 " + thread.getName()
+ " 的异常:" + e.getMessage()));
t.start();
t.join();
System.out.println("main 正常结束");
}
}
```text
输出:
```text
捕获到 问题线程 的异常:子线程炸了
main 正常结束
每个线程有自己独立的调用栈,异常只会沿着自己的栈往上抛。抛到 run() 顶端还没被处理,线程就默默死掉了,主线程毫无察觉。
不设置处理器的话,JVM 会把栈信息打到控制台。但生产环境如果没人看控制台,这个错误就等于消失了。所以后台任务一定要在 run() 内部用 try-catch 兜住,或者注册 UncaughtExceptionHandler。
关于虚拟线程
Java 21 正式引入了虚拟线程,这是并发编程的一次重大升级。
传统线程和操作系统线程一一对应,创建成本高,开几千个就吃不消。虚拟线程由 JVM 管理,一个操作系统线程可以承载成千上万个虚拟线程,创建开销几乎可以忽略。
用法非常简单:
Thread vt = Thread.ofVirtual().start(() ->
System.out.println("我是虚拟线程:" + Thread.currentThread()));
```bash
它适合 I/O 密集型场景,比如每个请求开一个线程去查数据库。以前这么写会撑爆线程数,现在完全可行。
> [!NOTE]
> 虚拟线程属于进阶内容,入门阶段了解有这么个东西就够了。先把普通线程的原理和线程安全搞扎实,再回头看它会轻松很多。
## 本节小结
- `start()` 开启新线程,`run()` 只是普通方法调用,不产生并发。
- `start()` 只能调用一次,线程结束后无法重启。
- 优先用 `Runnable` 或 Lambda,避免继承 `Thread` 占掉唯一的继承位。
- 给线程起名字,排查问题时事半功倍。
- `sleep()` 暂停当前线程,`join()` 等待目标线程结束。
- `Callable` 有返回值,配合 `Future.get()` 获取结果。
- 多线程的执行顺序不确定,代码不能依赖它。
- Java 21 的虚拟线程适合海量 I/O 任务,属于进阶话题。