什么是 Runnable 接口?
Runnable 是 Java 中一个函数式接口,它代表一个可以被线程执行的任务,它只有一个抽象方法 run()。
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
核心要点:
- 任务与线程分离:
Runnable的核心思想是将“要执行的任务”(run方法的代码)和“执行任务的机制”(Thread类)分离开来,这使得你可以在不同的线程中执行同一个任务,或者将同一个任务提交给不同的执行框架(如线程池)。 - 无返回值:
Runnable的run方法没有返回值 (void),这意味着如果你通过Runnable执行一个任务,并且该任务需要计算一个结果,你无法直接通过run方法返回这个结果,如果需要返回结果,应该使用Callable接口。 - 不处理检查型异常:
run方法不能抛出检查型异常(checked exception),IOException。run方法内部可能会抛出检查型异常,你必须在方法内部使用try-catch块来处理它,这是它与普通方法的一个重要区别。
如何使用 Runnable 接口?
使用 Runnable 通常遵循以下三个步骤:
- 创建任务类:定义一个类,实现
Runnable接口,并重写run()方法,将你要执行的任务代码放入run()方法中。 - 创建
Runnable实例:创建上面定义的任务类的对象。 - 创建线程并启动:将
Runnable实例作为参数传递给Thread类的构造函数,创建一个Thread对象,然后调用该对象的start()方法来启动线程。
示例代码
这是一个最经典的 "Hello World" 并发示例:
// 1. 创建任务类:实现 Runnable 接口
class MyTask implements Runnable {
private String taskName;
public MyTask(String name) {
this.taskName = name;
}
@Override
public void run() {
// 任务要执行的代码
for (int i = 1; i <= 5; i++) {
System.out.println(taskName + " is running, count: " + i);
try {
// 模拟任务耗时
Thread.sleep(500); // 休眠 500 毫秒
} catch (InterruptedException e) {
// 处理中断异常
System.out.println(taskName + " was interrupted.");
// 恢复中断状态(良好实践)
Thread.currentThread().interrupt();
return; // 中断后退出任务
}
}
System.out.println(taskName + " has finished.");
}
}
// 主程序
public class RunnableExample {
public static void main(String[] args) {
// 2. 创建 Runnable 实例
MyTask task1 = new MyTask("Task-A");
MyTask task2 = new MyTask("Task-B");
// 3. 创建线程并启动
Thread thread1 = new Thread(task1, "Thread-1");
Thread thread2 = new Thread(task2, "Thread-2");
System.out.println("Starting threads...");
thread1.start();
thread2.start();
System.out.println("Main thread continues its work...");
}
}
可能的输出结果(因为线程执行顺序不确定,所以每次运行结果可能不同):
Starting threads...
Main thread continues its work...
Task-A is running, count: 1
Task-B is running, count: 1
Task-A is running, count: 2
Task-B is running, count: 2
Task-A is running, count: 3
Task-B is running, count: 3
Task-A is running, count: 4
Task-B is running, count: 4
Task-A is running, count: 5
Task-B is running, count: 5
Task-A has finished.
Task-B has finished.
从输出可以看出,Task-A 和 Task-B 的代码是交替执行的,这证明了它们在不同的线程中并发运行,主线程 main 在启动了这两个线程后,也继续执行自己的代码,并没有阻塞。
使用 Lambda 表达式(Java 8+)
由于 Runnable 是一个函数式接口(只有一个抽象方法),我们可以使用更简洁的 Lambda 表达式来创建任务实例,而无需定义一个实现类。
上面的示例可以改写为:
public class RunnableLambdaExample {
public static void main(String[] args) {
// 使用 Lambda 表达式直接创建 Runnable 实例
Runnable task1 = () -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Lambda-Task-A is running, count: " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Lambda-Task-A has finished.");
};
// 同样可以创建另一个
Runnable task2 = () -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Lambda-Task-B is running, count: " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Lambda-Task-B has finished.");
};
// 创建并启动线程
Thread thread1 = new Thread(task1);
Thread thread2 = new Thread(task2);
thread1.start();
thread2.start();
}
}
这种方式代码更紧凑,是现代 Java 开发中推荐的做法。
Runnable vs. 继承 Thread 类
在 Java 中,除了实现 Runnable 接口,还可以通过继承 Thread 类来创建线程,这两种方式各有优缺点。
| 特性 | 实现 Runnable 接口 |
继承 Thread 类 |
|---|---|---|
| 核心思想 | 任务与执行分离,更灵活。 | 任务与执行绑定,耦合度高。 |
| 继承限制 | 没有限制,一个类可以实现多个接口,但不能继承多个类。 | 有限制,Java 是单继承的,继承了 Thread 类就不能再继承其他父类了。 |
| 资源共享 | 非常方便,多个 Thread 对象可以共享同一个 Runnable 实例,从而共享其实例变量。 |
不方便,每个 Thread 对象都有自己的副本,共享数据需要额外的同步机制。 |
| 代码复用 | 高。Runnable 实例可以被多个线程使用,也可以提交给线程池。 |
低。Thread 子类本身就是一个线程,复用性较差。 |
| 面向对象 | 更符合 OOP 设计原则,将“行为”(run 方法)和“实体”(Thread)分开。 |
耦合度高,将行为和实体混在一起。 |
| 推荐度 | 强烈推荐,这是实现多线程的标准和首选方式。 | 不推荐,除非有非常特殊的需求(需要重写 Thread 类的其他方法)。 |
在绝大多数情况下,都应该优先选择实现 Runnable 接口的方式来创建多线程。
Runnable 与线程池
Runnable 接口的重要性在现代 Java 并发编程中更加凸显,因为它与 Executor 框架(线程池)完美集成。
你不能直接将一个 Thread 对象提交给线程池,但你可以将一个 Runnable 任务提交给线程池,由池中的某个工作线程来执行。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class RunnableWithThreadPool {
public static void main(String[] args) {
// 创建一个固定大小为 2 的线程池
ExecutorService executor = Executors.newFixedThreadPool(2);
// 提交多个 Runnable 任务给线程池
executor.submit(() -> {
System.out.println("Task 1 is running in pool.");
});
executor.submit(() -> {
System.out.println("Task 2 is running in pool.");
});
executor.submit(() -> {
System.out.println("Task 3 is running in pool.");
});
// 关闭线程池
executor.shutdown(); // 等待所有任务完成后再关闭
// executor.shutdownNow(); // 立即停止所有正在执行的任务
}
}
在这个例子中,我们创建了 3 个 Runnable 任务,但线程池大小只有 2,线程池会管理这些任务的执行,无需我们手动创建和销毁 Thread 对象,大大提高了资源利用率和程序性能。
- 定义:
Runnable是一个任务接口,其run()方法定义了任务要执行的代码。 - 核心优势:解耦,它将任务逻辑与线程机制分离,使得代码更灵活、更易于复用和共享。
- 使用方式:创建
Runnable实例 -> 创建Thread对象(或提交给线程池)-> 启动。 - 现代实践:使用 Lambda 表达式 来创建
Runnable实例,代码更简洁。 - 最佳实践:优先选择实现
Runnable接口,而不是继承Thread类。 - 重要性与未来:
Runnable是Executor框架的基础,是理解和使用现代 Java 并发编程(如CompletableFuture、Stream并行处理)的基石,虽然Future和CompletableFuture提供了更强大的异步编程能力,但Runnable依然是定义简单、无返回值任务的基石。
