在 Java 多线程编程中,创建线程的方式主要有三种:继承Thread类、实现Runnable接口、实现Callable接口结合FutureTask。每种方式都有其独特的应用场景和特性,本文将通过具体代码示例和面试题解析深入分析。
一、通过继承 Thread 类实现线程
核心原理
继承Thread类并重写run()方法,run()内定义线程执行逻辑。通过start()方法启动线程,而非直接调用run()。
代码示例
package day13;
public class TestThread extends Thread {
@Override
public void run() {
// 获取当前线程名称(主线程为"main",子线程格式为"Thread-序号")
String threadName = Thread.currentThread().getName();
for (int i = 0; i < 10; i++) {
System.out.println(threadName + " 当前循环:" + i);
}
}
public static void main(String[] args) {
TestThread t1 = new TestThread();
TestThread t2 = new TestThread();
// 注意:直接调用run()会作为普通方法执行,不会创建新线程
// t1.run();
// t2.run();
// 正确方式:通过start()启动新线程
t1.start();
t2.start();
}
}
执行逻辑说明
- start () 与 run () 的本质区别
-
- start():触发 JVM 创建新线程,线程状态从NEW变为RUNNABLE(就绪态),由 CPU 调度执行run()方法,实现多线程并发。
-
- run():若直接调用,等同于主线程内的普通方法调用,按顺序执行,无并发效果。
二、通过实现 Runnable 接口实现线程
核心原理
将线程任务(run()方法)与线程对象(Thread)分离,解耦任务逻辑与线程管理,支持资源共享。
代码示例
public class TestRunnable implements Runnable {
// 共享变量,两个线程共同操作
private int sum = 0;
@Override
public void run() {
String threadName = Thread.currentThread().getName();
for (int i = 2; i <= 100; i += 2) {
sum += i;
System.out.println(threadName + " 当前累加和:" + sum);
}
}
public static void main(String[] args) {
// 创建共享任务实例
TestRunnable sharedTask = new TestRunnable();
// 创建两个线程共享同一个任务
Thread threadA = new Thread(sharedTask, "线程A");
Thread threadB = new Thread(sharedTask, "线程B");
threadA.start();
threadB.start();
// 使用Lambda表达式创建线程(Java 8+)
new Thread(() -> {
System.out.println(
Thread.currentThread().getName() + " 通过Lambda创建"
);
}, "线程C").start();
}
}
核心优势
- 资源共享:多个线程可共享同一个Runnable实例(如示例中的task),方便实现数据共享(如多线程售票系统中的共享票数)。
- 消除单继承限制:允许类在实现Runnable的同时继承其他类,提高代码扩展性。
- 函数式接口支持:可通过 Lambda 表达式简化代码,提升编程效率(如主线程中的匿名内部类简化)。
三、通过实现 Callable 接口结合 FutureTask 实现线程
核心原理
Callable是增强版Runnable,支持返回值和异常抛出,需通过FutureTask包装后与Thread配合使用。
代码示例
import java.util.concurrent.*;
public class TestCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
String threadName = Thread.currentThread().getName();
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
Thread.sleep(300); // 模拟耗时操作
System.out.println(threadName + " 当前累计和:" + sum);
}
return sum;
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
TestCallable task = new TestCallable();
FutureTask<Integer> futureTask = new FutureTask<>(task);
Thread workerThread = new Thread(futureTask, "计算线程");
workerThread.start();
// 阻塞等待计算结果
int total = futureTask.get();
System.out.println("1-100的总和为:" + total);
}
}
关键特性
- 返回值支持:call()方法可通过return返回泛型结果(如示例中的Integer),适用于需要异步计算结果的场景(如分布式计算、耗时 IO 操作)。
- 异常处理:允许抛出受检异常(如InterruptedException),调用端通过try-catch或声明抛出处理,比Runnable的run()更灵活。
- FutureTask 的作用:
-
- 作为Thread的参数,桥接Callable与线程;
-
- 提供get()方法阻塞获取结果、cancel()取消任务、isDone()判断任务是否完成等功能。
Runnable 与 Callable 的区别?
特性 |
Runnable |
Callable |
方法名 |
run()(无返回值) |
call()(有泛型返回值) |
异常处理 |
只能捕获异常,不能声明抛出 |
可声明抛出受检异常 |
返回值 |
无 |
支持泛型返回值 |
使用方式 |
直接作为Thread参数 |
需通过FutureTask包装后使用 |
四、三种方式对比与适用场景
创建方式 |
继承 Thread 类 |
实现 Runnable 接口 |
实现 Callable+FutureTask |
核心特点 |
简单直观,耦合度高 |
解耦任务与线程,支持资源共享 |
支持返回值和异常处理 |
继承限制 |
受单继承限制 |
无限制(可继承其他类) |
无限制 |
返回值 |
无 |
无 |
有(通过 FutureTask 获取) |
适用场景 |
简单独立任务 |
多线程资源共享场景 |
需要异步计算结果的场景 |
典型应用 |
独立日志输出线程 |
多线程售票系统 |
分布式计算结果汇总 |
总结
- Thread 类:适合简单独立任务,但受单继承限制,扩展性较弱。
- Runnable 接口:推荐优先使用,解耦任务逻辑与线程,支持资源共享和 Lambda 简化,适用于大多数多线程场景。
- Callable+FutureTask:在需要获取线程执行结果或处理异常时使用,如异步计算、任务调度等复杂场景。
掌握这三种创建方式的核心差异和适用场景,能帮助开发者在实际项目中更高效地利用多线程提升程序性能,同时应对面试中的高频问题。