文章提示
适合人群:具备Java基础、了解线程基本概念
你将学会:
线程创建的四种方式
Callable与Runnable的核心区别
FutureTask的实战应用技巧
避免常见多线程陷阱的方法
目录
文章提示
前言
一、线程创建的四大门派
1. 继承Thread类(青铜段位)
2. 实现Runnable接口(白银段位)
3. Callable+FutureTask(黄金组合)
4. 线程池(王者之选)
二、Runnable vs Callable 世纪对决
1. Callable 接口
2. FutureTask 类
1.代码示例:
2.代码分析:
总结
三、必知必会的两大特性
1. 结果缓存机制
2. 阻塞式获取结果
高频面试题精解
总结与升华
在Java多线程开发中,Runnable和Callable就像两位性格迥异的双胞胎兄弟。他们都能执行异步任务,但一个沉默寡言(没有返回值),一个活泼外向(能带回结果)。今天我们将通过生活化的比喻和实战代码,带你彻底掌握这对兄弟的差异,以及他们的黄金搭档——FutureTask的使用秘籍!
class MyThread extends Thread {
public void run() {
System.out.println("继承Thread方式");
}
}
// 使用
new MyThread().start();
class MyRunnable implements Runnable {
public void run() {
System.out.println("实现Runnable方式");
}
}
// 使用
new Thread(new MyRunnable()).start();
class MyCallable implements Callable {
public String call() throws Exception {
return "Callable返回值";
}
}
// 使用
FutureTask task = new FutureTask<>(new MyCallable());
new Thread(task).start();
System.out.println(task.get()); // 获取返回值
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.submit(() -> System.out.println("线程池方式"));
pool.shutdown();
想要了解更多可以参考作者的: JUC多线程:一篇文章搞懂线程池:从核心参数到源码设计全解析-CSDN博客
在 Java 中,Callable 接口和 FutureTask 类提供了比 Runnable 更强大的功能,特别是支持返回值和异常处理
定义:Callable 接口类似于 Runnable,但它可以返回一个结果,并且可以抛出受检异常。
方法:Callable 接口只有一个方法 call(),该方法返回一个泛型类型的结果,并且可以抛出 Exception。
定义:FutureTask 类实现了 Runnable 和 Future 接口,可以包装一个 Callable 对象,并提供异步执行任务的能力。
功能:
package JUC.Usafe;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 1、探究原理
* 2、觉自己会用
*/
public class CallableTest {
public static void main(String[] args) throws ExecutionException,
InterruptedException {
// new Thread(new Runnable()).start();
// new Thread(new FutureTask()).start();
// new Thread(new FutureTask( Callable )).start();
new Thread().start(); // 怎么启动Callable
MyThread thread = new MyThread();
FutureTask futureTask = new FutureTask(thread); // 适配类
new Thread(futureTask,"A").start();
new Thread(futureTask,"B").start(); // 结果会被缓存,效率高
Integer o = (Integer) futureTask.get(); //这个get 方法可能会产生阻塞!把他放到最后
// 或者使用异步通信来处理!
System.out.println(o);
}
}
class MyThread implements Callable {
@Override
public Integer call() {
System.out.println("call()"); // 会打印几个call
// 耗时的操作
return 1024;
}
}
CallableTest 类展示了如何使用 Callable 和 FutureTask 来执行任务并获取结果。以下是关键点分析:
MyThread 类实现 Callable 接口:
class MyThread implements Callable {
@Override
public Integer call() {
System.out.println("call()"); // 会打印几个call
// 耗时的操作
return 1024;
}
}
MyThread 类实现了 Callable
call() 方法打印 "call()" 并返回整数 1024。
CallableTest 类中的 main 方法
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread thread = new MyThread();
FutureTask futureTask = new FutureTask<>(thread); // 适配类
new Thread(futureTask, "A").start();
new Thread(futureTask, "B").start(); // 结果会被缓存,效率高
Integer o = futureTask.get(); // 这个get 方法可能会产生阻塞!把他放到最后
// 或者使用异步通信来处理!
System.out.println(o);
}
}
创建 Callable 实例:MyThread thread = new MyThread();
创建 FutureTask 实例:FutureTask
启动线程:
new Thread(futureTask, "A").start();
new Thread(futureTask, "B").start();
获取结果:Integer o = futureTask.get();
打印结果:System.out.println(o);
3.call() 方法的执行情况
线程 A 和线程 B 启动
两个线程 A 和 B 都启动并尝试执行 futureTask。
futureTask 包装了 MyThread 实例,因此两个线程都会调用 MyThread 的 call() 方法。
call() 方法的执行
第一次调用:假设线程 A 先获取到 futureTask 的锁并执行 call() 方法。
打印 "call()"
返回 1024
结果被缓存
第二次调用:线程 B 尝试执行 call() 方法,但由于结果已被缓存,call() 方法不会再次执行。
直接返回缓存的结果 1024
输出结果:
call() 方法的打印次数:
只会打印一次 "call()",因为 FutureTask 会缓存 Callable 任务的结果,后续的调用不会重新执行 call() 方法。
特性 | Runnable | Callable |
---|---|---|
返回值 | ❌ 无 | ✅ 有 |
异常处理 | ❌ 只能try-catch | ✅ 可抛出 |
方法签名 | void run() | V call() throws |
适用场景 | 简单异步任务 | 需要结果的复杂任务 |
FutureTask task = new FutureTask<>(() -> "结果只能获取一次");
new Thread(task).start();
System.out.println(task.get()); // 输出:结果只能获取一次
System.out.println(task.get()); // 仍然输出相同结果
// 注意:任务只会执行一次,多次get()获取的是缓存结果
FutureTask task = new FutureTask<>(() -> {
TimeUnit.SECONDS.sleep(5);
return 42;
});
new Thread(task).start();
System.out.println("开始等待结果...");
Integer result = task.get(); // 这里会阻塞5秒
System.out.println("得到结果:" + result);
Q:为什么Callable不能直接传给Thread?
A:Thread构造函数只接受Runnable类型。Callable需要FutureTask这个"适配器"来桥接,就像Type-C转接头能让USB设备连接手机一样。
Q:如何处理Callable的异常?
A:两种方式:
// 方式1:在call()方法内部处理
Callable c = () -> {
try {
// 可能出错的代码
} catch (Exception e) {
return "错误处理结果";
}
};
// 方式2:通过Future.get()捕获
try {
future.get();
} catch (ExecutionException e) {
Throwable cause = e.getCause(); // 获取原始异常
}
通过本文的学习,我们掌握了:
线程创建四大法
单线程简单任务用Thread/Runnable
需要返回值用Callable+FutureTask黄金组合
高并发场景必用线程池
Callable三大优势
能带回任务结果(就像外卖小哥送餐后带回确认签名)
支持异常传播(发现问题及时上报)
配合FutureTask实现异步回调
FutureTask使用秘籍
通过get()
获取结果时记得处理阻塞
利用缓存特性避免重复计算
结合线程池使用效果更佳
终极实战建议:
耗时计算任务(如Excel导出)使用Callable
需要聚合多个服务结果时用FutureTask集合
重要业务务必处理ExecutionException
扩展思考:当我们需要同时处理100个异步任务时,如何优化?这时就该请出CompletableFuture和Fork/Join框架这些高级装备了,它们能让多线程编程如虎添翼!