Java线程创建的四种方式
本文讲解了Java创建线程的四种方式:继承Thread、实现Runnable、实现Callable和使用线程池,并对比优劣,最终强烈推荐使用线程池。
在Java中创建线程主要有四种方式。我会从最基础到最推荐的方式逐一为你详细讲解,并附上代码示例和优缺点对比。
几种创建线程的方式概览
- 继承
Thread类:最直接的方式,但不推荐。 - 实现
Runnable接口:推荐的方式,将任务(Runnable)与线程(Thread)解耦。 - 实现
Callable接口(配合FutureTask):Runnable的增强版,可以有返回值,可以抛出异常。 - 使用线程池(
ExecutorService):强烈推荐的生产级实践方式,能有效管理和复用线程资源。
方式一:继承 Thread 类
这是最直观的方式,让一个类直接成为一个线程类。
步骤:
- 创建一个类,继承自
java.lang.Thread。 - 重写
run()方法,将线程需要执行的逻辑代码放在run()方法中。 - 创建该类的实例。
- 调用实例的
start()方法来启动线程(注意: 是调用start()而不是run()!调用run()只是一个普通的方法调用,不会创建新线程)。
代码示例:
java
// 1. 定义一个继承自 Thread 的类
class MyThread extends Thread {
// 2. 重写 run() 方法
@Override
public void run() {
System.out.println("通过继承 Thread 类创建的线程正在运行...");
System.out.println("当前线程名称: " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
// 3. 创建线程实例
MyThread thread1 = new MyThread();
// 4. 启动线程
thread1.start();
System.out.println("Main 线程结束。");
}
}
优缺点:
- 优点:实现简单,代码直观易懂。
- 缺点:
- 单继承局限:Java 不支持多重继承,如果你的类已经继承了另一个类,就无法再继承
Thread类了。 - 紧耦合:任务(
run方法中的逻辑)与线程的创建(Thread类)紧密地扣合在一起,不符合面向对象的设计原则。
- 单继承局限:Java 不支持多重继承,如果你的类已经继承了另一个类,就无法再继承
方式二:实现 Runnable 接口
这是更常用且推荐的方式,它将“任务”和“执行任务的线程”分离开。
步骤:
- 创建一个类,实现
java.lang.Runnable接口。 - 实现接口中的
run()方法,这里是线程的执行体。 - 创建该
Runnable实现类的实例。 - 创建一个
Thread对象,并将Runnable实例作为构造函数的参数传入。 - 调用
Thread对象的start()方法。
代码示例:
java
// 1. 定义一个实现 Runnable 接口的类
class MyRunnable implements Runnable {
// 2. 实现 run() 方法
@Override
public void run() {
System.out.println("通过实现 Runnable 接口创建的线程正在运行...");
System.out.println("当前线程名称: " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
// 3. 创建 Runnable 实例 (这是任务)
MyRunnable myRunnable = new MyRunnable();
// 4. 创建 Thread 实例,并将任务传递给它
Thread thread2 = new Thread(myRunnable);
// 5. 启动线程
thread2.start();
System.out.println("Main 线程结束。");
}
}
优缺点:
- 优点:
- 解耦:将任务逻辑(
Runnable)和线程机制(Thread)分离,结构清晰。 - 无继承限制:你的任务类可以继承其他任何类,因为它只是实现一个接口。
- 资源共享:多个线程可以共享同一个
Runnable实例,方便实现资源共享。
- 解耦:将任务逻辑(
- 缺点:相比继承
Thread类,代码稍微多一点点。
方式三:实现 Callable 接口
如果你的线程执行完任务后需要一个返回值,或者任务执行中可能抛出异常,Callable 是最好的选择。
步骤:
- 创建一个类,实现
java.util.concurrent.Callable<V>接口,其中V是返回值的类型。 - 实现
call()方法(注意不是run()),这个方法可以有返回值,也可以throws Exception。 - 创建
Callable实现类的实例。 - 使用
FutureTask<V>来包装Callable对象。FutureTask同时实现了Runnable和Future接口。 - 将
FutureTask对象作为参数创建Thread对象并启动。 - 通过
FutureTask对象的get()方法获取线程执行的返回值(get()方法会阻塞,直到任务完成)。
代码示例:
java
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ExecutionException;
// 1. 定义一个实现 Callable 接口的类
class MyCallable implements Callable<String> {
// 2. 实现 call() 方法,带返回值
@Override
public String call() throws Exception {
System.out.println("通过实现 Callable 接口创建的线程正在运行...");
Thread.sleep(2000); // 模拟耗时操作
return "任务完成,这是返回值!";
}
}
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 3. 创建 Callable 实例
MyCallable myCallable = new MyCallable();
// 4. 使用 FutureTask 包装 Callable
FutureTask<String> futureTask = new FutureTask<>(myCallable);
// 5. 创建并启动线程
Thread thread3 = new Thread(futureTask);
thread3.start();
System.out.println("Main 线程继续执行其他任务...");
// 6. 获取线程返回值 (此方法会阻塞)
String result = futureTask.get();
System.out.println("获取到的返回值: " + result);
System.out.println("Main 线程结束。");
}
}
优缺点:
- 优点:
- 可以有返回值:能够获取异步任务的执行结果。
- 可以抛出异常:可以在
call方法中抛出受检异常,并在主线程中捕获。
- 缺点:实现稍微复杂一些。
方式四:使用线程池 (ExecutorService)
在实际企业级开发中,这是最推荐的方式。手动创建和销毁线程开销很大,线程池可以复用已创建的线程,统一管理,避免资源耗尽。
步骤:
- 使用
Executors工厂类创建ExecutorService(线程池)。常见的有newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor。 - 创建你的任务(
Runnable或Callable的实例)。 - 调用线程池的
submit()或execute()方法提交任务。execute(Runnable): 提交不需要返回值的任务。submit(Runnable/Callable): 提交任务,返回一个Future对象,可以用来获取结果或检查状态。
- (重要)当不再需要线程池时,调用
shutdown()方法平滑地关闭它。
代码示例:
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
public static void main(String[] args) throws Exception {
// 1. 创建一个固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 提交 Runnable 任务
executorService.execute(() -> {
System.out.println("线程池中的线程(Runnable)正在执行... " + Thread.currentThread().getName());
});
// 提交 Callable 任务
Future<Integer> future = executorService.submit(() -> {
System.out.println("线程池中的线程(Callable)正在执行... " + Thread.currentThread().getName());
return 100;
});
// 获取 Callable 任务的结果
Integer result = future.get();
System.out.println("Callable 任务的返回结果: " + result);
// 4. 关闭线程池
executorService.shutdown(); // 不再接收新任务,但会执行完已提交的任务
}
}
上面的例子也使用了 Lambda 表达式,这是 Java 8 以来的语法糖,可以极大地简化 Runnable 和 Callable 的匿名内部类写法。
优缺点:
- 优点:
- 资源管理:统一管理线程生命周期,降低资源消耗。
- 性能提升:通过复用线程,避免了频繁创建和销毁线程的开销。
- 控制并发:可以控制同时并发的线程数量,防止系统过载。
- 功能强大:提供了定时执行、定期执行、获取结果等丰富功能。
- 缺点:理解和使用比前几种方式稍微复杂。
总结与最佳实践
| 创建方式 | 核心 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|---|
继承 Thread |
extends Thread |
简单直观 | 单继承限制、耦合度高 | ★☆☆☆☆ (不推荐) |
实现 Runnable |
implements Runnable |
解耦、无继承限制、可资源共享 | 代码稍多 | ★★★★☆ (推荐) |
实现 Callable |
implements Callable |
可返回结果、可抛出异常 | 实现稍复杂 | ★★★★☆ (特定场景推荐) |
| 使用线程池 | ExecutorService |
资源复用、性能高、易于管理、功能强大 | 学习成本稍高 | ★★★★★ (强烈推荐) |
最佳实践建议:
- 首选线程池:在任何需要处理多个异步任务的生产环境中,都应该优先考虑使用线程池。
- 任务选择:
- 如果任务不需要返回值,使用
Runnable。 - 如果任务需要返回值或可能抛出异常,使用
Callable。
- 如果任务不需要返回值,使用
- 拥抱 Lambda:在 Java 8及以上版本中,使用 Lambda 表达式来创建
Runnable和Callable实例,会让代码更简洁、更具可读性。 - 理解基础:尽管不推荐直接使用
extends Thread,但理解它是学习Java多线程的基础。