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. Future 与 ScheduledFuture 的区别
在前面的示例中可以看到,ScheduledThreadPoolExecutor
返回的是 ScheduledFuture
类型。
✅ ScheduledFuture
继承自 Future
和 Delayed
接口,因此具备以下特点:
- 继承
Future
的基本能力(如get()
获取结果) - 实现
Delayed
接口,新增getDelay()
方法,可获取剩余延迟时间 - 可进一步扩展为
RunnableScheduledFuture
,支持判断任务是否是周期性的
✅ ScheduledThreadPoolExecutor
内部通过 ScheduledFutureTask
类实现这些接口,用于管理和控制任务的生命周期。
8. 总结
本文我们介绍了多种在 Java 中启动线程、执行并发任务的方法,并重点分析了 Timer
与 ScheduledThreadPoolExecutor
的差异。
✅ 总体来看:
方式 | 特点 | 是否推荐 |
---|---|---|
直接继承 Thread | 简单直观,适合学习 | ❌ 不适合生产 |
ExecutorService | 线程池管理、复用、性能好 | ✅ 推荐 |
CompletableFuture | 链式异步、非阻塞 | ✅ 推荐 |
Timer | 老旧、单线程、易出错 | ❌ 不推荐 |
ScheduledThreadPoolExecutor | 多线程、高可靠性、灵活配置 | ✅ 强烈推荐 |
本教程源码可以在 GitHub 查看:点击这里