创建线程的 3 种方式:从基础到线程池,一篇讲透
面试官:“Java 中有几种方式创建线程?”
你:“三种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口(带返回值)。但实际开发中推荐使用线程池,避免频繁创建销毁线程的开销。”
面试官:“那 Runnable 和 Callable 有什么区别?线程池有哪些好处?”
你:“……”
很多人能说出三种方式,但一追问底层原理、区别、线程池优势就含糊了。本文从代码示例到原理分析,彻底讲透 Java 创建线程的几种方式。
一、概述
在 Java 中,线程是并发执行的基本单元。创建线程的三种主流方式:
| 方式 | 返回结果 | 能否抛出异常 | 特点 |
|---|---|---|---|
| 继承 Thread | 无 | 否 | 简单,但单继承限制 |
| 实现 Runnable | 无 | 否 | 更灵活,推荐 |
| 实现 Callable | 有(Future) | 可以 | 可获取返回结果,支持异常 |
无论哪种方式,核心都是创建一个Thread对象并调用start()方法,由 JVM 调度执行。
二、方式一:继承 Thread 类
1. 实现步骤
- 定义一个类继承
Thread。 - 重写
run()方法,编写需要执行的任务。 - 创建该类的实例,调用
start()启动线程。
2. 代码示例
publicclassMyThreadextendsThread{@Overridepublicvoidrun(){System.out.println("线程 "+Thread.currentThread().getName()+" 执行");}}// 使用MyThreadt=newMyThread();t.start();// 启动线程,JVM 调用 run()3. 缺点
- 单继承限制:Java 不支持多继承,如果已经继承了其他类,就无法再使用此方法。
- 任务与线程耦合:不利于任务共享和复用。
- 无法获得返回值、无法抛异常。
三、方式二:实现 Runnable 接口
1. 实现步骤
- 定义类实现
Runnable接口,重写run()方法。 - 创建
Thread对象,将Runnable实例作为构造参数传入。 - 调用
Thread.start()启动。
2. 代码示例
publicclassMyRunnableimplementsRunnable{@Overridepublicvoidrun(){System.out.println("线程 "+Thread.currentThread().getName()+" 执行");}}// 使用Runnabletask=newMyRunnable();Threadt=newThread(task);t.start();3. 优点
- 避免了单继承的限制(可以实现多个接口)。
- 任务与线程解耦,同一个
Runnable可以被多个线程执行,易于资源共享。 - 更适合线程池(线程池直接接受
Runnable或Callable)。
4. Runnable 与 Thread 的对比
Thread本身实现了Runnable接口,但通常我们选择实现Runnable。- 实现
Runnable将任务代码与线程运行机制分离,符合面向接口设计原则。
四、方式三:实现 Callable 接口
1. 特点
- 位于
java.util.concurrent包。 - 有返回值(通过
Future获取)。 - 可以抛出受检异常。
- 通常与
ExecutorService和Future配合使用。
2. 实现步骤
- 定义类实现
Callable<V>,重写call()方法。 - 将
Callable实例提交给ExecutorService,返回Future对象。 - 调用
Future.get()获取执行结果(会阻塞直到完成)。
3. 代码示例
importjava.util.concurrent.*;publicclassMyCallableimplementsCallable<String>{@OverridepublicStringcall()throwsException{Thread.sleep(1000);return"执行结果:"+Thread.currentThread().getName();}}// 使用ExecutorServiceexecutor=Executors.newSingleThreadExecutor();Future<String>future=executor.submit(newMyCallable());try{Stringresult=future.get();// 阻塞等待System.out.println(result);}catch(InterruptedException|ExecutionExceptione){e.printStackTrace();}finally{executor.shutdown();}4. 与 Runnable 的区别
| 特性 | Runnable | Callable |
|---|---|---|
| 方法 | run() | call() |
| 返回值 | 无(void) | 有(V) |
| 异常 | 不能抛出受检异常 | 可以抛出受检异常 |
| 使用方式 | Thread或ExecutorService.submit(Runnable) | 仅ExecutorService.submit(Callable) |
五、对比总结
| 方式 | 代码复杂程度 | 灵活性 | 返回值 | 适用场景 |
|---|---|---|---|---|
| 继承 Thread | 简单 | 低(单继承) | 无 | 简单小任务,不推荐 |
| 实现 Runnable | 中等 | 高 | 无 | 大多数并发任务,线程池基础 |
| 实现 Callable | 中等 | 高 | 有 | 需要返回结果或抛出异常的任务 |
六、为什么不建议直接创建线程?—— 线程池的优势
直接new Thread().start()创建线程在小型 Demo 中可行,但生产环境存在严重问题:
- 资源消耗:每次创建和销毁线程都需要系统调用(内核态切换),开销大。
- 无限制创建导致 OOM:并发量大时,无限创建线程会耗尽内存或导致系统崩溃。
- 缺乏统一管理:线程无法复用,也没有监控、限流、定时等功能。
线程池(Executor framework)的优势
- 线程复用:核心线程一直存活,任务执行完不销毁,减少创建销毁开销。
- 流量控制:通过队列和最大线程数限制并发,防止资源耗尽。
- 功能丰富:支持定时任务、周期任务、提交 Callable 返回 Future。
- 优雅关闭:可以平滑关闭线程池,等待已有任务完成。
示例:使用线程池执行 Runnable 和 Callable
ExecutorServicepool=Executors.newFixedThreadPool(10);// 执行 Runnablepool.execute(()->System.out.println("Runnable task"));// 提交 Callable 并获取 FutureFuture<Integer>future=pool.submit(()->{Thread.sleep(1000);return42;});pool.shutdown();实际开发中,几乎不会手动创建 Thread,而是通过 Spring 的线程池或自己配置 ThreadPoolExecutor。
七、常见面试追问
Q1:start()和run()的区别?
run()只是普通方法调用,不会启动新线程。start()会启动新线程,由 JVM 调度执行run()方法。
Q2:Runnable 和 Callable 如何选择?
- 不需要返回值或抛异常 →
Runnable。 - 需要返回值或抛异常 →
Callable+Future。
Q3:线程池有哪几种?如何配置?
常用的工厂方法:
newFixedThreadPool(int n):固定线程数。newCachedThreadPool():按需创建,空闲线程存活 60 秒。newSingleThreadExecutor():单线程。newScheduledThreadPool(int corePoolSize):定时/周期任务。
实际生产中,建议直接使用ThreadPoolExecutor构造方法自定义核心参数(corePoolSize、maximumPoolSize、keepAliveTime、workQueue、handler),避免无界队列导致 OOM。
Q4:如何获取线程执行结果?
使用Callable+Future(或CompletableFuture)。
Q5:线程池的拒绝策略有哪些?
AbortPolicy(默认):抛出RejectedExecutionException。CallerRunsPolicy:让调用者线程自己执行。DiscardPolicy:静默丢弃任务。DiscardOldestPolicy:丢弃等待队列中最旧的任务,然后重试提交。
八、总结
| 方式 | 核心要点 |
|---|---|
| 继承 Thread | 简单但单继承局限,不推荐 |
| 实现 Runnable | 解耦任务与线程,推荐 |
| 实现 Callable | 有返回值,可抛异常,配合线程池使用 |
| 线程池 | 生产环境唯一推荐,避免自己 new Thread() |
一句话记住创建线程的方式:继承实现都可以,Runnable 更常用,Callable 带返回;线程池复用性能好,千万莫把手动搞。
理解创建线程的方式及其背后的资源管理,是写出高可用并发程序的基础。希望这篇文章能帮你彻底掌握这个高频考点,欢迎继续讨论。