首页 / Java 入门教程 / 创建线程

Java 入门教程

创建线程

本教程共 100 篇 · 第 94 篇 · 更新于 2026-08-05 · 约 14 分钟阅读

JavaJava 入门教程ThreadRunnableCallablestart方法

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
1100 求和 = 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 任务,属于进阶话题。