Thread、Runnable与Callable的区别及推荐用法
目前golang学习网上已经有很多关于文章的文章了,自己在初次阅读这些文章中,也见识到了很多学习思路;那么本文《创建线程的常见方式有三种:继承Thread类、实现Runnable接口、使用Callable接口。其中,实现Runnable接口更推荐,因为它避免了Java单继承的限制,代码结构更清晰,便于资源共享和任务解耦。》,也希望能帮助到大家,如果阅读完后真的对你学习文章有帮助,欢迎动动手指,评论留言并分享~
使用ExecutorService线程池是创建线程的最佳方式,因其能有效管理资源、控制并发、复用线程并提供任务队列和高级抽象,避免频繁创建线程带来的性能开销与系统风险,同时支持Callable返回结果和统一生命周期管理,适用于绝大多数生产场景。

在Java中,创建线程主要有三种方式:继承 Thread 类、实现 Runnable 接口,以及利用 ExecutorService 线程池。如果问哪种方式更好,我个人会毫不犹豫地推荐使用 ExecutorService 线程池。它不仅提供了更高级的抽象,能够更好地管理线程资源,还能有效避免直接创建和管理线程带来的诸多问题。当然,理解前两种基础方式是深入掌握并发编程的基石,它们各有适用场景,但对于大多数实际的生产环境应用,线程池无疑是更健壮、更高效的选择。
解决方案
创建线程的几种核心方式各有其哲学和应用场景。我们来逐一剖析。
1. 继承 Thread 类
这是最直观的方式之一。你创建一个新类,让它继承自 java.lang.Thread,然后重写 run() 方法,将线程的执行逻辑放在这个方法里。
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello from a custom Thread!");
// 线程执行的业务逻辑
}
}
// 使用
MyThread thread = new MyThread();
thread.start(); // 启动线程这种方式的优点是简单明了,代码量少。但缺点也很明显:Java是单继承的,如果你的业务类已经继承了其他类,就无法再继承 Thread 了。这在实际项目中是很大的限制。此外,将任务逻辑与线程本身紧密耦合,使得代码复用性变差。
2. 实现 Runnable 接口
这是更常用也更推荐的基础方式。你创建一个类实现 java.lang.Runnable 接口,然后实现 run() 方法。这个 Runnable 对象可以作为参数传递给 Thread 类的构造器,再由 Thread 对象来启动。
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Hello from a Runnable task!");
// 线程执行的业务逻辑
}
}
// 使用
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start(); // 启动线程相较于继承 Thread,实现 Runnable 的优势在于:
- 解耦: 任务(
Runnable)与线程(Thread)是分离的,任务可以独立于任何线程类存在。 - 单继承限制: 你的业务类可以自由继承其他类,因为接口可以多实现。
- 资源共享: 多个线程可以共享同一个
Runnable实例,这对于需要共享数据的场景很有用。
3. 使用 ExecutorService (线程池)
这是现代Java并发编程的主流方式,也是我个人认为“更好”的方式。ExecutorService 是 java.util.concurrent 包提供的高级并发工具,它管理着一个线程池,负责线程的创建、销毁和复用。你只需将任务提交给 ExecutorService,它会自行安排线程来执行。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// 任务仍然是实现 Runnable 接口
class MyPooledTask implements Runnable {
private String taskName;
public MyPooledTask(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
System.out.println("Executing task: " + taskName + " on thread: " + Thread.currentThread().getName());
// 模拟耗时操作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// 使用
// 创建一个固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
executor.submit(new MyPooledTask("Task-" + i)); // 提交任务
}
executor.shutdown(); // 关闭线程池,等待所有任务执行完毕使用线程池的好处是压倒性的:
- 资源管理: 避免了频繁创建和销毁线程的开销,提高了系统性能。
- 控制并发: 可以限制并发线程的数量,防止资源耗尽。
- 任务队列: 未执行的任务会排队等待,平滑处理突发流量。
- 更高级的抽象: 提供了
Callable接口,可以返回执行结果(通过Future对象)并抛出受检异常,这是Runnable不具备的。 - 统一管理: 方便对线程进行监控、统计和关闭。
为什么说使用线程池管理线程是更优的选择?
在我看来,选择线程池并非仅仅是“好一点”,而是现代并发编程的基石。它解决了直接创建线程时面临的诸多痛点,从系统设计的角度看,是一种更成熟、更健壮的方案。
首先,资源开销是首要考量。每次 new Thread() 都会涉及操作系统层面的资源分配和销毁,这开销不小。想象一下,如果你的应用每秒需要处理几百个短生命周期的任务,每次都创建新线程,系统的负担会迅速飙升,最终可能导致性能瓶颈甚至崩溃。线程池通过线程复用机制,将这些开销降到最低。它预先创建好一定数量的线程,当有任务到来时,直接从池中取出空闲线程执行,任务完成后线程归还池中,等待下一个任务。这就像一个高效的工人团队,无需每次都招聘新员工,而是让现有员工轮流处理工作。
其次,并发控制是线程池的另一个核心价值。直接创建线程时,你很难有效控制同时运行的线程数量。如果任务量激增,无限创建线程可能迅速耗尽系统资源(如内存、CPU),导致“线程爆炸”。线程池允许你设定最大并发线程数,未执行的任务会进入等待队列。这提供了一个天然的流量削峰机制,保证系统在面对高并发时依然能够稳定运行,不至于瞬间崩溃。比如,一个Web服务器,如果每个请求都创建一个新线程,很容易在高负载下变得不稳定,而使用线程池就能优雅地处理并发请求。
再者,任务管理与扩展性。ExecutorService 不仅支持 Runnable 这种“只管执行,不关心结果”的任务,还引入了 Callable 接口。Callable 任务可以返回一个结果,并且可以抛出异常,这使得异步任务的处理更加灵活和强大。配合 Future 对象,我们甚至可以取消任务、检查任务是否完成,或者阻塞等待任务结果。这对于需要进行复杂计算并获取结果的场景非常有用。例如,你可能需要并行计算多个子任务,然后汇总它们的结果,Callable 和 Future 就是为此而生的。
最后,统一的生命周期管理和可观测性。直接创建的线程,其生命周期管理相对分散,你很难统一关闭所有线程或监控它们的运行状态。而线程池提供了一套标准的生命周期管理API(如 shutdown()、shutdownNow()、awaitTermination()),可以优雅地关闭所有线程,确保资源被正确释放。同时,许多监控工具和框架也能更好地与线程池集成,提供更细粒度的性能指标和状态报告。这对于生产环境的运维和故障排查至关重要。
在实际开发中,如何根据任务特性选择合适的线程创建方式?
选择合适的线程创建方式,实际上是在权衡简单性、性能、资源管理和复杂性。这需要我们深入理解任务本身的特性以及系统对并发的需求。
1. 简单、一次性的独立任务(通常不推荐直接使用)
如果你有一个非常简单、生命周期极短、且确定只运行一次的任务,理论上你可以选择继承 Thread 或实现 Runnable。例如,一个简单的后台日志清理任务,或者一个启动时进行初始化检查的线程。
- 继承
Thread: 如果你的任务类不需要继承其他类,且任务逻辑与线程本身耦合度高(或者说,你觉得这样写最直观),可以考虑。但即便如此,我也倾向于用Runnable。 - 实现
Runnable: 这是更好的选择,因为它将任务逻辑与线程执行机制分离。即使是简单任务,这种解耦也能带来更好的代码组织和复用性。
然而,我的建议是:即使是这种场景,也应该考虑使用 Executors.newSingleThreadExecutor()。 它能提供一个单线程的线程池,既能保证任务按序执行,又能享受线程池的资源管理和生命周期控制,避免了手动创建 Thread 带来的潜在问题。
2. 大多数业务场景:需要高效管理、复用线程,控制并发的任务
这几乎涵盖了所有生产环境中的并发场景,包括但不限于:Web服务器的请求处理、异步消息处理、后台批处理任务、并行计算、定时任务等。
ExecutorService是不二之选。 在这种情况下,关键在于选择合适的线程池类型:Executors.newFixedThreadPool(int nThreads): 适用于CPU密集型任务。线程数通常设置为CPU核心数或核心数+1。它会创建固定数量的线程,如果任务多于线程数,任务会进入队列等待。- 示例场景: 大量需要进行复杂计算、数据处理的任务。
Executors.newCachedThreadPool(): 适用于I/O密集型任务或任务数量波动大的场景。它会根据需要创建新线程,如果线程空闲时间超过一定阈值(60秒),则会被回收。线程数没有上限。- 示例场景: 网络请求、数据库操作、文件读写等,这些任务大部分时间在等待I/O操作完成,CPU占用不高。
Executors.newSingleThreadExecutor(): 适用于需要保证所有任务按提交顺序依次执行的场景。它内部只有一个工作线程。- 示例场景: 顺序日志写入、资源访问需要严格串行化的任务。
Executors.newScheduledThreadPool(int corePoolSize): 适用于需要定时执行或周期性执行任务的场景。- 示例场景: 定时数据同步、周期性报告生成。
3. 需要获取任务执行结果或处理异常的任务
如果你提交的任务不仅需要执行,还需要返回一个结果,或者你希望能够捕获任务执行过程中抛出的异常。
ExecutorService配合Callable和Future。Callable接口允许call()方法返回一个值,并抛出异常。ExecutorService.submit(Callable会返回一个task) Future对象,你可以通过future.get()获取任务结果或捕获异常。
import java.util.concurrent.*;
class MyCallableTask implements Callable<String> {
private String name;
public MyCallableTask(String name) {
this.name = name;
}
@Override
public String call() throws Exception {
System.out.println("Callable task " + name + " started.");
Thread.sleep(200); // 模拟耗时操作
if (Math.random() < 0.2) {
throw new RuntimeException("Error in task " + name);
}
return "Result from " + name;
}
}
// 使用
ExecutorService executor = Executors.newFixedThreadPool(3);
Future<String> future1 = executor.submit(new MyCallableTask("Task A"));
Future<String> future2 = executor.submit(new MyCallableTask("Task B"));
try {
System.out.println(future1.get()); // 阻塞直到任务完成并获取结果
System.out.println(future2.get());
} catch (InterruptedException | ExecutionException e) {
System.err.println("Task failed: " + e.getMessage());
} finally {
executor.shutdown();
}总结一下,在实际开发中,除非有非常特殊的理由(例如,你正在编写一个底层的并发库,需要对 Thread 有极致的控制),否则我几乎总是推荐使用 ExecutorService。它将线程管理的复杂性从你的业务逻辑中抽象出来,让你能够专注于任务本身,同时提供了强大的性能和稳定性保证。
创建线程时常见的误区有哪些,以及如何避免?
在并发编程中,创建和管理线程远非表面看起来那么简单。许多开发者在实践中会踩到一些坑,这些误区如果不加以注意,轻则影响性能,重则导致系统崩溃或数据错误。
1. 无限制地创建新线程(线程爆炸)
这是最常见也最危险的误区之一。当每个请求或每个任务都 new Thread() 时,系统很快就会因为创建过多线程而耗尽资源。每个线程都需要占用一定的内存(线程栈),并且CPU在大量线程之间切换(上下文切换)也会带来巨大的开销。
- 如何避免: 几乎所有场景都应该使用线程池(
ExecutorService)。通过线程池,你可以限制并发线程的数量,将超出的任务放入队列等待,从而保护系统资源。选择合适的线程池类型和大小是关键。
2. 忽视线程安全问题
当多个线程访问和修改共享数据时,如果没有适当的同步机制,就可能出现竞态条件(Race Condition)、数据不一致等问题。这通常是并发编程中最难调试的bug。
- 如何避免:
- 最小化共享数据: 尽可能让每个任务处理自己的数据,减少对共享状态的依赖。
- 使用同步机制: 对共享资源的访问进行同步。Java提供了多种工具:
synchronized关键字(方法或代码块)。java.util.concurrent.locks.Lock接口及其实现类(如ReentrantLock),提供更灵活的锁定机制。java.util.concurrent.atomic包下的原子类(如AtomicInteger),用于对基本类型或引用进行原子操作。java.util.concurrent包下的并发集合(如ConcurrentHashMap),它们是线程安全的。
- 使用不可变对象: 不可变对象一旦创建就不能修改,天然线程安全。
3. 错误地终止线程(使用 Thread.stop() 等废弃方法)
Thread.stop()、Thread.suspend()、Thread.resume() 等方法已经被标记为废弃(deprecated)并且非常危险。它们可能导致线程在执行到一半时突然停止,从而释放未完成的锁,造成数据不一致或死锁。
- 如何避免: 应该使用协作式中断机制。
- 通过
Thread.interrupt()方法向线程发送中断请求。 - 在线程的
run()或call()方法中,定期检查Thread.currentThread().isInterrupted()标志。 - 当捕获到
InterruptedException时,通常应该重新设置中断标志 (Thread.currentThread().interrupt();) 并决定如何响应(例如,优雅地退出任务)。
- 通过
4. 线程中未捕获的异常
如果一个线程在执行 run() 或 call() 方法时抛出了一个未捕获的异常,并且没有设置 UncaughtExceptionHandler,那么这个异常会直接导致线程终止,但不会传播到主线程,可能会默默地导致程序状态异常。
- 如何避免:
- 在
run()或call()方法内部使用try-catch块捕获所有可能的异常。 - 为线程设置
Thread.UncaughtExceptionHandler,以便在线程因未捕获异常而终止时进行处理(例如,记录日志、重启任务)。 - 对于
Callable任务,通过Future.get()获取结果时,如果任务抛出异常,get()方法会抛出ExecutionException,可以从中获取原始异常。
- 在
5. 混淆 Thread.start() 和 Thread.run()
新手常犯的错误是调用 Thread 对象的 run() 方法而不是 start() 方法。调用 run() 方法只会把 run() 方法当作一个普通方法在当前线程中执行,并不会启动一个新的线程。
- 如何避免: 始终调用
Thread.start()方法来启动新线程。
6. 不正确地关闭线程池
如果应用程序退出时没有正确关闭线程池,可能会导致线程资源泄露,或者应用程序无法正常退出,因为后台线程池还在运行。
- 如何避免: 在应用程序关闭时,调用
ExecutorService的shutdown()方法。shutdown()会阻止新任务提交,并等待已提交任务执行完成。如果需要立即停止所有任务,可以使用shutdownNow()。通常会结合awaitTermination()来等待线程池中的任务完成。
executor.shutdown(); // 拒绝新任务,等待已提交任务完成
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { // 等待60秒
executor.shutdownNow(); // 强制关闭
if (!executor.awaitTermination(60, TimeUnit.SECONDS))
System.err.println("Pool did not terminate");
}
} catch (InterruptedException ie) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}理解并避免这些误区,是写出健壮、高效并发程序的关键。并发编程的复杂性在于其非确定性,因此在设计和实现时,需要格外小心和细致。
好了,本文到此结束,带大家了解了《Thread、Runnable与Callable的区别及推荐用法》,希望本文对你有所帮助!关注golang学习网公众号,给大家分享更多文章知识!
动态下拉列表PHP实现方法详解
- 上一篇
- 动态下拉列表PHP实现方法详解
- 下一篇
- Gobig包接收者修改模式解析
-
- 文章 · java教程 | 3小时前 |
- Java代码风格统一技巧分享
- 107浏览 收藏
-
- 文章 · java教程 | 4小时前 | java 格式化输出 字节流 PrintStream System.out
- JavaPrintStream字节输出方法解析
- 362浏览 收藏
-
- 文章 · java教程 | 4小时前 |
- ThreadLocalRandom提升并发效率的原理与实践
- 281浏览 收藏
-
- 文章 · java教程 | 4小时前 |
- 身份证扫描及信息提取教程(安卓)
- 166浏览 收藏
-
- 文章 · java教程 | 5小时前 |
- JavaCopyOnWriteArrayList与Set使用解析
- 287浏览 收藏
-
- 文章 · java教程 | 5小时前 |
- Java线程安全用法:CopyOnWriteArrayList详解
- 136浏览 收藏
-
- 文章 · java教程 | 5小时前 |
- Java流收集后处理:Collectors.collectingAndThen用法解析
- 249浏览 收藏
-
- 文章 · java教程 | 6小时前 |
- staticfinal变量初始化与赋值规则解析
- 495浏览 收藏
-
- 文章 · java教程 | 6小时前 |
- 判断两个Map键是否一致的技巧
- 175浏览 收藏
-
- 文章 · java教程 | 6小时前 | java 空指针异常 空值判断 requireNonNull Objects类
- JavaObjects空值判断实用技巧
- 466浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ChatExcel酷表
- ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
- 3193次使用
-
- Any绘本
- 探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
- 3405次使用
-
- 可赞AI
- 可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
- 3436次使用
-
- 星月写作
- 星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
- 4543次使用
-
- MagicLight
- MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
- 3814次使用
-
- 提升Java功能开发效率的有力工具:微服务架构
- 2023-10-06 501浏览
-
- 掌握Java海康SDK二次开发的必备技巧
- 2023-10-01 501浏览
-
- 如何使用java实现桶排序算法
- 2023-10-03 501浏览
-
- Java开发实战经验:如何优化开发逻辑
- 2023-10-31 501浏览
-
- 如何使用Java中的Math.max()方法比较两个数的大小?
- 2023-11-18 501浏览

