Java 中创建线程的多种方法详解
前言
多线程编程是 Java 并发编程的核心内容之一。Java 从语言层面和标准库层面提供了多种创建线程的方式,每种方式都有其适用的场景和优劣。本文将详细介绍 Java 中常见的五种创建线程的方法,并通过代码示例和对比分析帮助你全面掌握它们。
一、继承 Thread 类
原理
Thread 类本身实现了 Runnable 接口,通过继承 Thread 并重写其 run() 方法,即可定义一个线程任务。创建实例后调用 start() 方法启动线程。
代码示例
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 运行中...");
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " : " + i);
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start(); // 启动线程(不能直接调用 run())
t2.start();
}
}
特点
| 优势 | 劣势 |
|---|---|
| 简单直观,适合快速实现 | Java 单继承,继承了 Thread 就无法继承其他类 |
可以直接使用 Thread 类的方法(如 getName()) |
任务与线程耦合在一起,复用性差 |
| 适合简单的单次任务 | 多个线程共享同一任务时不够灵活 |
二、实现 Runnable 接口
原理
定义一个类实现 Runnable 接口,实现 run() 方法,然后将该实例作为参数传递给 Thread 构造器。这是推荐的方式,因为 Java 支持接口的多实现。
代码示例
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 运行中...");
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " : " + i);
}
}
}
public class RunnableDemo {
public static void main(String[] args) {
MyRunnable task = new MyRunnable();
Thread t1 = new Thread(task, "线程-A");
Thread t2 = new Thread(task, "线程-B");
t1.start();
t2.start();
}
}
特点
| 优势 | 劣势 |
|---|---|
| 解耦:任务与线程分离,同一个 Runnable 实例可被多个线程执行 | 没有返回值 |
| 可以继承其他类(接口的灵活性) | 无法抛出受检异常(checked exception) |
| 更适合线程池场景 | 代码略显冗余(需要额外创建 Thread 对象) |
三、实现 Callable 接口 + FutureTask
原理
Runnable 的缺陷是没有返回值且不能抛出受检异常。Callable<V> 接口解决了这个问题:它的 call() 方法有返回值,且可以抛出异常。配合 FutureTask(它实现了 RunnableFuture,即同时是 Runnable 和 Future)可以将 Callable 交给线程执行并获取结果。
代码示例
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + " 计算中...");
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
// 模拟耗时
Thread.sleep(10);
}
return sum; // 返回 1+2+...+100 的结果
}
}
public class CallableDemo {
public static void main(String[] args) {
// 创建 Callable 任务
MyCallable callable = new MyCallable();
// 包装成 FutureTask(Runnable 的实现类)
FutureTask<Integer> futureTask = new FutureTask<>(callable);
// 交给线程执行
Thread t = new Thread(futureTask, "计算线程");
t.start();
// 主线程可以继续做其他事情...
System.out.println("主线程继续工作...");
try {
// 获取 Callable 的返回结果(阻塞等待)
Integer result = futureTask.get();
System.out.println("计算结果: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
特点
| 优势 | 劣势 |
|---|---|
支持返回值,通过 Future.get() 获取 |
Future.get() 是阻塞操作,可能造成线程阻塞 |
| 支持抛出受检异常,调用方可以捕获处理 | 代码稍复杂,需要额外创建 FutureTask |
支持取消任务(Future.cancel()) |
不直接配合线程池使用时不够简洁 |
四、线程池(ExecutorService)
原理
通过 java.util.concurrent.Executors 工厂类创建线程池,提交 Runnable 或 Callable 任务。线程池复用线程,避免频繁创建/销毁线程的开销,是生产环境中最推荐的方式。
代码示例
import java.util.concurrent.*;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 1. 创建固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
// 2. 提交 Runnable 任务
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " 执行 Runnable 任务");
});
// 3. 提交 Callable 任务(有返回值)
Future<Integer> future = executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " 执行 Callable 任务");
return 42;
});
try {
Integer result = future.get(2, TimeUnit.SECONDS);
System.out.println("Callable 返回结果: " + result);
} catch (Exception e) {
e.printStackTrace();
}
// 4. 批量提交任务
for (int i = 0; i < 5; i++) {
int taskId = i;
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() +
" 执行任务 #" + taskId);
});
}
// 5. 关闭线程池(优雅关闭)
executor.shutdown();
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
executor.shutdownNow(); // 强制关闭
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
}
}
常见的线程池类型
| 工厂方法 | 特点 | 适用场景 |
|---|---|---|
newFixedThreadPool(n) |
固定 n 个线程,任务队列无界 | 线程数固定的场景 |
newCachedThreadPool() |
可缓存的线程池,按需创建,自动回收闲置线程 | 大量短期异步任务 |
newSingleThreadExecutor() |
单线程,保证任务按顺序执行 | 需要顺序执行的任务 |
newScheduledThreadPool(n) |
支持定时/周期性任务 | 定时任务、延迟任务 |
newWorkStealingPool() |
JDK 8+,工作窃取算法,并行度与 CPU 核数相关 | 大任务拆分为小任务并行处理 |
⚠️ 生产环境建议:直接使用
ThreadPoolExecutor构造器显式指定参数,而非Executors工厂方法。因为工厂方法的默认参数(如Integer.MAX_VALUE的队列长度或最大线程数)可能在极端场景下引发 OOM。
五、Lambda 表达式 / 匿名内部类
原理
Runnable 和 Callable 都是函数式接口(Functional Interface,只包含一个抽象方法的接口),因此可以直接使用 Lambda 表达式或匿名内部类来简化代码,无需定义单独的类。
代码示例
public class LambdaThreadDemo {
public static void main(String[] args) {
// ----- 方式 1:匿名内部类 + Runnable -----
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 匿名内部类");
}
}, "匿名字段-A");
// ----- 方式 2:Lambda + Runnable(最简洁)-----
Thread t2 = new Thread(() ->
System.out.println(Thread.currentThread().getName() + " Lambda"),
"Lambda-B");
// ----- 方式 3:Lambda + 线程池 -----
ExecutorService pool = Executors.newCachedThreadPool();
pool.execute(() -> System.out.println(
Thread.currentThread().getName() + " 线程池 + Lambda"));
// ----- 方式 4:Lambda + Callable -----
Future<String> future = pool.submit(() -> {
Thread.sleep(500);
return "Callable 返回结果";
});
// ----- 方式 5:方法引用(Method Reference)-----
pool.execute(LambdaThreadDemo::printMessage);
t1.start();
t2.start();
pool.shutdown();
}
private static void printMessage() {
System.out.println(Thread.currentThread().getName() + " 方法引用");
}
}
特点
| 优势 | 劣势 |
|---|---|
| 代码极度简洁,无需额外定义类 | 逻辑复杂时不易阅读 |
| 适合简单、一次性的任务 | 调试时堆栈信息不够直观 |
| 与线程池配合使用非常方便 | 相同逻辑无法复用 |
六、五种方法横向对比
| 维度 | extends Thread |
implements Runnable |
Callable + FutureTask |
线程池 ExecutorService |
Lambda / 匿名 |
|---|---|---|---|---|---|
| 代码简洁度 | ⭐⭐⭐ | ⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 任务与线程解耦 | ❌ | ✅ | ✅ | ✅ | ✅ |
| 支持返回值 | ❌ | ❌ | ✅ | ✅ | ✅ (配合 Callable) |
| 支持异常抛出 | ❌ | ❌ | ✅ | ✅ | ✅ |
| 线程复用 | ❌ | ❌ | ❌ | ✅ | ❌ |
| 推荐使用场景 | 学习/简单 demo | 单次任务、小型项目 | 需要获取异步结果 | 生产环境首选 | 简单快速实现 |
七、总结与建议
-
学习阶段:四种方式都应掌握,理解线程的本质 ——
Thread是线程的抽象,Runnable/Callable是任务的抽象。 -
小型项目或简单场景:使用
Runnable+ Lambda 最为简洁高效。 -
需要获取异步结果:使用
Callable+FutureTask或配合线程池的submit()方法。 -
生产环境:始终优先使用线程池(
ThreadPoolExecutor或ExecutorService),避免直接new Thread()。线程池可以控制并发度、复用线程、减少资源开销。 -
避免继承
Thread:除非你需要重写Thread类中除run()以外的其他方法(如getName()等可以不依赖当前线程上下文直接获取),否则优先实现Runnable。
“Favor composition over inheritance.” —— 在创建线程这件事上,优先实现
Runnable/Callable接口而非继承Thread类,是这条设计原则最好的体现。