1. 引言

在本教程中,我们将探讨几种启动线程并执行并发任务的方式。

尤其是在处理耗时操作或需要周期性执行的任务时,将它们放在主线程之外运行至关重要,否则会阻塞 UI 或影响用户体验。

如果你对线程生命周期感兴趣,可以参考我们之前的教程:Java 线程的生命周期

2. 启动线程的基础方式

我们可以通过 Java 的 Thread 类来轻松地实现一个并行执行的逻辑。

来看一个简单的例子,通过继承 Thread 类:

public class NewThread extends Thread {
    public void run() {
        long startTime = System.currentTimeMillis();
        int i = 0;
        while (true) {
            System.out.println(this.getName() + ": New Thread is running..." + i++);
            try {
                // 每次打印间隔一秒,避免刷屏太快
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ...
        }
    }
}

然后我们写一个主类来初始化并启动这个线程:

public class SingleThreadExample {
    public static void main(String[] args) {
        NewThread t = new NewThread();
        t.start();
    }
}

⚠️ 注意:我们必须在处于 NEW 状态(即尚未启动)的线程上调用 start() 方法。否则,Java 会抛出 IllegalThreadStateException 异常。

接着,如果我们需要启动多个线程:

public class MultipleThreadsExample {
    public static void main(String[] args) {
        NewThread t1 = new NewThread();
        t1.setName("MyThread-1");
        NewThread t2 = new NewThread();
        t2.setName("MyThread-2");
        t1.start();
        t2.start();
    }
}

这段代码看起来非常简单,也和网上大多数教程类似。

❌ 但请注意:这离生产环境可用还差得远,因为在真实项目中,资源管理、上下文切换开销、内存使用控制等都非常重要

✅ 所以要达到生产级质量,我们需要额外编写大量模板代码来处理:

  • 统一线程创建机制
  • 控制并发线程数量
  • 线程回收:特别是守护线程,防止内存泄漏

当然我们可以自己实现这些功能,但为什么要重复造轮子呢?

3. 使用 ExecutorService 框架

ExecutorService 实现了线程池设计模式(也叫 Worker 模式),它帮我们自动管理线程的创建与销毁,并提供了诸如线程复用、任务队列等强大特性。

线程复用尤其重要:在大规模应用中,频繁创建和销毁线程对象会造成显著的内存管理负担。而使用工作线程池则能有效降低这种开销。

此外,ExecutorService 还支持灵活配置,例如:

  • 队列类型
  • 最小/最大线程数
  • 线程命名规则等

更多关于 ExecutorService 的内容,请参考我们的另一篇文章:Java ExecutorService 指南

4. 使用 Executors 提交任务

有了这个强大的框架,我们可以从“启动线程”的思维转变为“提交任务”。

来看一个异步任务提交的例子:

ExecutorService executor = Executors.newFixedThreadPool(10);
...
executor.submit(() -> {
    new Task();
});

我们可以使用两个方法:

  • execute(Runnable):无返回值
  • submit(Callable<T>)submit(Runnable):返回 Future<T>,用于获取计算结果

想深入了解 Future?请看这篇:java.util.concurrent.Future 指南

5. 使用 CompletableFuture 提交任务

如果我们要从 Future 获取最终结果,通常调用 get() 方法,但这会导致主线程阻塞直到任务完成。

为了避免阻塞,我们可以添加回调逻辑,但这无疑增加了代码复杂度。

✅ Java 8 引入了一个更现代的异步编程工具 —— CompletableFuture,它基于 Future 构建,实现了 CompletionStage 接口。

它提供了丰富的链式操作方法,让我们可以优雅地绑定回调,无需手动处理复杂的异步流程。

示例代码如下:

CompletableFuture.supplyAsync(() -> "Hello");

这里的 supplyAsync() 接收一个 Supplier 参数,也就是我们要异步执行的逻辑(这里是一个 Lambda 表达式)。

✅ 默认情况下,任务会被提交到 ForkJoinPool.commonPool() 执行,当然你也可以传入自定义的 Executor 作为第二个参数。

了解更多 CompletableFuture 内容,请查阅:CompletableFuture 完整指南

6. 延迟或定时执行任务

在复杂的 Web 应用中,我们常常需要在特定时间点或者定期执行某些任务。

Java 提供了两种主要工具来实现这类需求:

  • java.util.Timer
  • java.util.concurrent.ScheduledThreadPoolExecutor

6.1. Timer

Timer 是一个用于在后台线程调度任务的工具类。

它可以安排一次性任务或周期性任务。

来看一个延迟一秒后执行任务的例子:

TimerTask task = new TimerTask() {
    public void run() {
        System.out.println("Task performed on: " + new Date() + "\n" 
          + "Thread's name: " + Thread.currentThread().getName());
    }
};
Timer timer = new Timer("Timer");
long delay = 1000L;
timer.schedule(task, delay);

再来加一个周期性任务:

timer.scheduleAtFixedRate(repeatedTask, delay, period);

此时任务会在指定延迟后首次执行,并按给定周期重复执行。

更多细节请参阅:Java Timer 教程

6.2. ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor 提供了类似于 Timer 的功能,但更加健壮:

ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
ScheduledFuture<Object> resultFuture
  = executorService.schedule(callableTask, 1, TimeUnit.SECONDS);

结束示例时,我们使用 scheduleAtFixedRate() 来安排周期性任务:

ScheduledFuture<Object> resultFuture
 = executorService.scheduleAtFixedRate(runnableTask, 100, 450, TimeUnit.MILLISECONDS);

6.3. 哪个更好?

虽然上面两个工具都能实现类似的效果,但选择哪个取决于底层机制。

让我们深入比较一下:

Timer 的问题:

❌ 不提供实时保证:内部使用 Object.wait(long)
❌ 单线程模型:任务串行执行,长时间任务会影响其他任务
❌ 异常处理差:一旦 TimerTask 抛出异常,整个 Timer 就挂了

ScheduledThreadPoolExecutor 的优势:

✅ 支持多线程
✅ 可利用多核 CPU
✅ 更好的异常处理机制(可重写 afterExecute
✅ 出现异常只取消当前任务,不影响其他任务
✅ 更精确的时间控制(依赖操作系统调度)
✅ 支持多任务协作(如等待所有任务完成)
✅ 更完善的线程生命周期管理 API

✅ 所以结论很明显:推荐使用 ScheduledThreadPoolExecutor 而非 Timer

7. FutureScheduledFuture 的区别

在前面的示例中可以看到,ScheduledThreadPoolExecutor 返回的是 ScheduledFuture 类型。

ScheduledFuture 继承自 FutureDelayed 接口,因此具备以下特点:

  • 继承 Future 的基本能力(如 get() 获取结果)
  • 实现 Delayed 接口,新增 getDelay() 方法,可获取剩余延迟时间
  • 可进一步扩展为 RunnableScheduledFuture,支持判断任务是否是周期性的

ScheduledThreadPoolExecutor 内部通过 ScheduledFutureTask 类实现这些接口,用于管理和控制任务的生命周期。

8. 总结

本文我们介绍了多种在 Java 中启动线程、执行并发任务的方法,并重点分析了 TimerScheduledThreadPoolExecutor 的差异。

✅ 总体来看:

方式 特点 是否推荐
直接继承 Thread 简单直观,适合学习 ❌ 不适合生产
ExecutorService 线程池管理、复用、性能好 ✅ 推荐
CompletableFuture 链式异步、非阻塞 ✅ 推荐
Timer 老旧、单线程、易出错 ❌ 不推荐
ScheduledThreadPoolExecutor 多线程、高可靠性、灵活配置 ✅ 强烈推荐

本教程源码可以在 GitHub 查看:点击这里


原始标题:How to Start a Thread in Java