在 Java 多线程开发中,Thread 和 Runnable 是最常被初学者混淆的两个接口/类。它们都能启动新线程,但设计哲学与使用方式大不相同。本文从源码、实践与设计角度,系统剖析两者的区别与选型建议。
一、Runnable 是接口,Thread 是类
-
Runnable 是一个函数式接口,仅定义了一个方法 run()。
-
Thread 是继承自 Object 的类,并实现了 Runnable 接口。
@FunctionalInterface
public interface Runnable {
void run();
}
public class Thread implements Runnable {
private Runnable target;
public void run() {
if (target != null) {
target.run();
}
}
}
✅ 本质:Thread 是对 Runnable 的包装。
二、使用方式对比:继承 vs 实现
方式一:继承 Thread 类
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread 方式运行");
}
}
new MyThread().start();
方式二:实现 Runnable 接口
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable 方式运行");
}
}
Thread thread = new Thread(new MyRunnable());
thread.start();
推荐使用 Runnable 实现方式,更灵活、可复用。
三、线程启动原理:Thread 与 Runnable 的交互
Thread 构造函数接收一个 Runnable 实例并将其赋值给 target:
public Thread(Runnable target) {
this.target = target;
}
当你调用 thread.start() 后,JVM 会启动新线程执行 Thread.run() 方法。其内部判断是否有 target,如果有则调用其 run():
@Override
public void run() {
if (target != null) {
target.run();
} else {
// 执行自身的 run() 逻辑
}
}
✅ 结论:Thread 只是线程的载体,真正的任务由 Runnable 描述。
四、设计哲学:职责分离与组合优雅
-
继承 Thread:将线程行为与业务逻辑耦合,不利于继承其他类(Java 不支持多继承)。
-
实现 Runnable:职责分离,符合组合优于继承的设计原则。
推荐模式:
Runnable task = () -> System.out.println("异步任务");
new Thread(task).start();
五、实战建议与最佳实践
维度 | Thread | Runnable |
---|---|---|
可扩展性 | 差(继承限制) | 高(可自由组合) |
代码复用 | 差 | 好 |
与线程池集成 | 不可直接使用 | 完美适配 |
实现复杂任务 | 不推荐 | 推荐 |
函数式支持 | 不支持 | 支持 Lambda |
✅ 实际开发中,应首选 Runnable 接口或 Callable(带返回值)配合线程池使用。
六、拓展话题:从 Runnable 到线程池
Runnable 是构建线程池的基础:
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> {
// 可配置任务逻辑
});
在 Java 8+ 中,推荐配合 CompletableFuture 与 Lambda 简化异步任务管理。
七、Callable接口与Future的配套使用
背景:
Runnable 无法获取线程执行结果,也无法抛出受检异常。为此,Java 引入了 Callable<V> 与 Future<V>。
核心原理:
-
Callable 是一个带返回值的任务定义接口。
-
Future 是一个异步计算结果的持有者,通过线程池提交任务时获取。
ExecutorService executor = Executors.newSingleThreadExecutor();
Callable<String> task = () -> {
Thread.sleep(1000);
return "任务完成";
};
Future<String> future = executor.submit(task);
// 阻塞等待结果
System.out.println(future.get());
executor.shutdown();
适用场景:
-
异步任务需要结果;
-
支持异常处理;
-
批量并发计算(如线程并行计算合并结果)。
八、ThreadPoolExecutor的核心线程模型
核心结构图:
提交任务 → 工作队列(BlockingQueue) → 核心线程(corePoolSize) → 最大线程(maximumPoolSize) → 拒绝策略
基本构造:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
✅ 参数说明:
-
corePoolSize:核心线程数,始终保留;
-
maximumPoolSize:最大线程数;
-
keepAliveTime:非核心线程空闲超时时间;
-
workQueue:阻塞队列,缓冲提交任务;
-
handler:拒绝策略。
✅ 推荐配置建议:
// CPU 密集型
new ThreadPoolExecutor(nThreads, nThreads, ...);
// IO 密集型
new ThreadPoolExecutor(nThreads * 2, nThreads * 4, ...);
九、synchronized与Lock的使用差异
关键区别:
特性 | synchronized | ReentrantLock |
---|---|---|
代码级别 | JVM 关键字 | Java 类 |
可中断性 | 不支持 | 支持 lockInterruptibly |
公平锁 | 不支持 | 支持 |
尝试获取(超时) | 不支持 | tryLock 支持 |
条件队列 | 不支持 | 支持 Condition |
示例对比:
// synchronized
synchronized(this) {
// 临界区
}
// ReentrantLock
Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区
} finally {
lock.unlock();
}
使用建议:
-
简单同步:优先使用 synchronized;
-
可中断/复杂并发控制:使用 ReentrantLock。
十、Java 21 的虚拟线程(VirtualThread)机制
概念:
虚拟线程是 JDK 21 正式引入的轻量级线程,由用户态调度器(ForkJoinPool)管理,旨在大幅提升高并发任务处理能力。
创建方式:
Runnable task = () -> System.out.println("虚拟线程执行");
Thread.startVirtualThread(task);
核心优势:
-
每个任务一个线程(无需池化);
-
非阻塞同步(适配结构化并发);
-
避免线程资源耗尽问题。
应用场景:
-
高并发 IO 服务;
-
Web 服务(Tomcat / Spring MVC 未来支持);
-
替代传统线程池设计。
十一、ForkJoinPool 与并行流的工作窃取原理
核心概念:
ForkJoinPool 是 Java 并发框架的高性能线程池,采用工作窃取算法(Work Stealing),适用于任务拆分与并行执行。
工作流程:
-
拆分任务(fork)成子任务;
-
子任务执行并将结果合并(join);
-
空闲线程可从其他线程的任务队列中“窃取”任务执行。
示例:
ForkJoinPool pool = new ForkJoinPool();
RecursiveTask<Integer> task = new RecursiveTask<>() {
protected Integer compute() {
if (/* 小任务 */) return result;
invokeAll(subtask1, subtask2);
return subtask1.join() + subtask2.join();
}
};
int result = pool.invoke(task);
并行流底层:
List<Integer> list = IntStream.range(1, 1000).boxed().collect(Collectors.toList());
int sum = list.parallelStream().mapToInt(i -> i).sum();
底层即由 ForkJoinPool.commonPool() 支持。