创建线程的几种方式
候选人小张在面试快手 P6 时,面试官看了一眼简历上"有丰富多线程开发经验",直接问道:
"Java 创建线程有几种方式?"
小张不假思索:"三种!继承 Thread、实现 Runnable、实现 Callable。"
面试官追问:"那你用 lambda 表达式创建 Runnable 算哪一种?线程池呢?"
小张愣了一下:"应该算实现 Runnable..."面试官继续:"Thread 的 start() 和 run() 方法有什么区别?为什么必须用 start() 而不是直接调用 run()?"
小张开始擦汗...
一、核心问题:创建线程的几种方式 🔴
1.1 问题拆解
这道题表面上是考 API 背诵,实际上考察的是候选人对"线程是什么"和"方法调用 vs 线程启动"的本质理解。面试官的追问链往往从最简单的入口切入,逐步深入:
1.2 ❌ 错误示范
候选人原话 A:"有三种方式:继承 Thread、实现 Runnable、实现 Callable。"
问题诊断:遗漏了线程池、ExecutorService、CompletableFuture、 ForkJoinPool 等实际生产中最重要的方式。只知道学校教的"三种方式",说明没有实际用过线程池。
候选人原话 B:"start() 和 run() 是一样的,都是执行线程。"
问题诊断:大错特错。start() 创建一个新线程并在新线程中执行 run(),而直接调用 run() 是在当前线程中同步执行 run() 方法体。多线程变成了单线程,这个 bug 在生产中极难排查。
候选人原话 C:"实现 Runnable 比继承 Thread 好,因为 Java 是单继承。"
问题诊断:这个理由太教科书了。真正的原因在于:Runnable 实现了逻辑与线程生命周期的解耦——同一个 Runnable 可以被多个 Thread 实例使用,实现了线程间共享目标对象。
1.3 标准回答
P5 级别:准确列举所有方式
Java 中创建线程的方式主要有以下几种:
- 继承 Thread 类,重写
run()方法- 实现 Runnable 接口,重写
run()方法- 实现 Callable 接口,配合 FutureTask 或线程池
- 线程池(
Executors.newFixedThreadPool()、newCachedThreadPool()等)- 匿名内部类或 lambda 表达式(本质是 Runnable)
- ForkJoinPool(JDK 7 引入,适合分治任务)
- CompletableFuture(JDK 8 异步编程)
其中,生产环境推荐使用线程池,因为直接
new Thread()的问题在于:每次创建新线程都有创建/销毁的开销(Thread 对象、栈空间、内核对象),线程数量不可控会耗尽系统资源。
这个回答展示了完整的知识面,已经比大多数候选人强。
P6 级别:深入原理与对比
Thread vs Runnable 的本质区别:
Runnable 的优势在于:
- 可以继承其他类(类结构更灵活)
- 多个线程可以共享同一个 Runnable 实例(实现"同一任务多人并行")
- 便于线程池管理
start() vs run() 的核心差异:
start()底层做了什么?
- 检查线程状态(必须是 NEW,否则抛
IllegalThreadStateException)- 通过本地方法
registerNatives()向 JVM 注册线程- JVM 创建 OS 线程(调用
pthread_create())- JVM 调用
Thread.run()在新线程中执行Callable vs Runnable 的关键区别:
这个回答展示了原理理解和代码对比,已经达到 P6 要求。
P7 级别:线程池与生产实践
生产环境的正确做法——线程池:
线程池的本质:线程是稀有资源,创建和销毁成本高(OS 线程的创建涉及内核态调用、栈空间分配、ThreadLocal 初始化)。线程池通过"预创建"和"复用"策略,将线程创建成本均摊到大量任务上。
不同线程池的选择场景:
【面试官心理】
这道题我能问到 P7 级别,是因为创建线程看似简单,但背后涉及 JVM 线程模型、OS 线程创建成本、线程池设计哲学。我最想看到的是候选人有"资源意识"——知道无节制创建线程的危害,知道用线程池管理线程。如果他还能提到 CompletableFuture 替代回调地狱,或者提到 JDK 21 的虚拟线程(Virtual Threads),那说明他有技术前沿的关注。
1.4 追问升级
追问 1:ThreadLocal 在 new Thread() 和线程池中有什么区别?
每个线程有独立的 ThreadLocalMap,
Thread.start()创建新线程时,ThreadLocalMap 是全新的。但在线程池中,线程被复用——上一次任务的 ThreadLocal 值可能残留到下一次任务,导致数据污染。生产中必须在线程复用场景下显式调用 ThreadLocal.remove(),或在afterExecute()中清理。
追问 2:为什么线程池不推荐使用 Executors 工厂方法?
阿里的 Java 规范明确禁止使用
Executors.newFixedThreadPool(n)和Executors.newCachedThreadPool()的原因:FixedThreadPool的LinkedBlockingQueue默认是Integer.MAX_VALUE,任务堆积会导致 OOM;CachedThreadPool的maximumPoolSize无上限,高并发下可能创建数万个线程。应该使用new ThreadPoolExecutor(...)显式配置参数。
追问 3:CompletableFuture 是什么?它和 Thread 的关系是什么?
CompletableFuture是 JDK 8 引入的异步编程工具,不是线程创建工具。它默认使用ForkJoinPool.commonPool()执行异步任务(线程数 = CPU 核心数 - 1)。它解决的问题是:回调地狱(多层嵌套)和结果聚合(thenCombine、thenCompose)。注意:CompletableFuture.supplyAsync()可以指定 Executor,不指定则用默认线程池。
二、lambda 与匿名内部类的坑 🟡
2.1 lambda 的本质
new Thread(() -> System.out.println("hello"))实际上是一个只有一个抽象方法的接口(SAM 接口)实例化,编译器将其转换为invokedynamic字节码指令,在首次执行时动态生成一个Runnable实现类。关键点:lambda 不创建新的类文件(对比匿名内部类),而是运行时动态生成类,所以 lambda 的性能通常更好(但差异微乎其微)。
2.2 闭包陷阱
三、生产避坑
3.1 线程池配置不当导致服务雪崩
场景:某服务使用 Executors.newCachedThreadPool() 接收外部请求,外部系统故障后请求堆积,线程池无限制创建线程,最终导致 OOM。
根因:高并发 + 无界队列 + 无上限线程数 = 服务爆炸。
正确配置:
3.2 ForkJoinPool 的错误使用
ForkJoinPool 适合"分治后归约"场景(如并行排序),不适合"大量独立短任务"场景。强行用它做普通多线程,效果反而不如普通线程池。
面试加分点:能说出"JDK 21 虚拟线程(Virtual Threads)彻底改变了线程的创建成本。虚拟线程的栈是堆内存分配的(默认 1MB,可动态扩容),创建成本接近普通对象的创建,而不是 OS 线程的创建"。这说明你关注了 JDK 的演进方向。
面试陷阱:被问到"start() 能否被调用两次"时,很多候选人会说"可以"。错!第二次调用 start() 会抛出 IllegalThreadStateException,因为线程状态已经从 NEW 变为其他状态。start() 方法中有一行 threadStatus != 0 的检查。