在Java并发编程中,仅会使用线程池是远远不够的。为了充分发挥多线程的威力,我们还需要深入理解Callable、Future和FutureTask的内在机制,并且能熟练地手写一个功能完备的线程池。让我们一同攻克这座高峰,领略并发编程的无穷魅力!
一、Callable和Future: 异步编程的钥匙
在传统的多线程编程中,我们仅能通过Runnable提交没有返回值的简单任务。而Callable则赋予了我们创建"有返回值的任务"的能力,为异步编程的大门打开了一扇窗。
Future则是Callable的完美绑定,通过它我们不仅能拿到任务的执行结果,还能判断任务的执行状态,甚至是取消任务。随着异步编程日益普及,掌握这对搭档就变得尤为重要。
Callable
接口和 Future
接口经常一起使用来执行异步任务。
Callable
接口与 Runnable
接口类似,但它可以返回结果并且可以抛出异常。
Future
接口代表异步计算的结果,并且可以用来检查计算是否完成,取消计算,以及获取计算的结果。
以下是一个使用 Callable
和 Future
完成异步排序任务的示例代码,其中包含异步取消、异常处理和超时控制:
import java.util.Arrays;
import java.util.concurrent.*;
public class AsyncSortExample {
public static void main(String[] args) {
// 创建一个线程池
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
// 创建一个 Callable 任务,执行排序操作
Callable<Integer[]> callable = () -> {
try {
// 模拟耗时的排序任务
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 假设我们对数组进行排序
Integer[] data = {5, 3, 8, 1, 2};
Arrays.sort(data);
return data;
};
// 提交任务并获取 Future 对象
Future<Integer[]> future = executor.submit(callable);
// 模拟其他任务或操作...
// ...
// 超时控制,等待结果,设置超时时间为 3 秒
try {
Integer[] sortedData = future.get(3, TimeUnit.SECONDS);
System.out.println("Sorted data: " + Arrays.toString(sortedData));
} catch (TimeoutException e) {
System.out.println("Task timed out, attempting to cancel...");
// 超时后尝试取消任务
future.cancel(true);
} catch (CancellationException e) {
System.out.println("Task was cancelled.");
} catch (ExecutionException e) {
// 处理 Callable 任务中的异常
System.out.println("Task threw an exception: " + e.getCause().getMessage());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭线程池
executor.shutdown();
}
}
}
代码解释:
- 线程池创建:使用
Executors.newSingleThreadExecutor()
创建了一个单线程的线程池。 - Callable 任务定义:定义了一个
Callable
任务,该任务模拟了一个耗时的排序操作。 - 任务提交:通过
executor.submit(callable)
提交任务,并获得一个Future
对象。 - 超时控制:使用
future.get(3, TimeUnit.SECONDS)
尝试获取结果,设置了 3 秒的超时时间。 - 异常处理:
TimeoutException
:如果任务执行超过 3 秒,将捕获此异常,并尝试取消任务。CancellationException
:如果任务已经被取消,将捕获此异常。ExecutionException
:如果 Callable 任务中抛出了异常,将捕获ExecutionException
并通过e.getCause()
获取原始异常。
- 取消任务:如果需要取消任务,可以使用
future.cancel(true)
,其中true
参数表示中断正在执行的任务。 - 关闭线程池:在
finally
块中关闭线程池,释放资源。
二、FutureTask源码剖析
FutureTask的出现,为Callable和Future之间架起了一座桥梁。作为两者的融合体,它不仅可以扮演Callable执行的载体角色,自身也是一个Future实例。
- FutureTask并不是直接实现Future接口的,而是通过RunnableFuture接口来间接实现- - RunnableFuture接口很简单,就是继承了Runnable和Future接口。
- 最上层的FunctionalInterface是一个注解,拥有该注解的接口支持函数式编程,图中是Runnable接口拥有该注解。
1、Future接口
public interface Future<V> {
// 任务取消方法
boolean cancel(boolean mayInterruptIfRunning);
// 判断任务是否取消
boolean isCancelled();
// 判断任务是否完成
boolean isDone();
// 阻塞获取任务结果
V get() throws InterruptedException, ExecutionException;
// 阻塞获取任务结果,并设置阻塞超时时间
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
2、RunnableFuture接口
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
3、FutureTask 内部原理
(1)、FutureTask 属性
public class FutureTask<V> implements RunnableFuture<V> {
// 状态机:存在以下7中状态
private volatile int state;
// 新建
private static final int NEW = 0;
// 任务完成中
private static final int COMPLETING = 1;
// 任务正常完成
private static final int NORMAL = 2;
// 任务异常
private static final int EXCEPTIONAL = 3;
// 任务取消
private static final int CANCELLED = 4;
// 任务中断中
private static final int INTERRUPTING = 5;
// 任务已中断
private static final int INTERRUPTED = 6;
// 支持结果返回的任务
private Callable<V> callable;
// 任务执行结果:包含正常和异常的结果,通过get方法获取
private Object outcome;
// 任务执行线程
private volatile Thread runner;
// 栈结构的等待队列,该节点是栈中的最顶层节点
private volatile WaitNode waiters;
}
(2)、FutureTask属性说明
- state状态机是FutureTask用于标记任务执行的状态情况,在源码中作者也描述了这些状态可能的变化请:
- 任务正常执行:NEW -> COMPLETING -> NORMAL
- 任务执行异常:NEW -> COMPLETING -> EXCEPTIONAL
- 任务被取消:NEW -> CANCELLE
- 任务被中断:NEW -> INTERRUPTING -> INTERRUPTED
-
outcome是任务执行完后的返回结果
-
runner是真正执行任务的worker
-
waiters是一个等待节点,而是是最顶层节点,类似头节点。FutureTask中的等待队列主要作用用于当多线程同时请求get方法获取结果时,这些线程可能要同时阻塞,因此将这些线程的阻塞信息保存在了等待节点中,并构成一个栈的等待结构。
-
Callable``FutureTask
封装了一个
Callable
任务,并且维护了任务的执行状态和结果。它内部使用AtomicInteger
类型的state
变量来表示任务的状态,状态的取值有:NEW
(0): 初始状态,任务还未开始执行COMPLETING
(1): 任务正在执行,即将完成NORMAL
(2): 任务正常完成EXCEPTIONAL
(3): 任务执行异常CANCELLED
(4): 任务被取消。INTERRUPTED
(5): 任务执行被中断
4、FutureTask 同步状态转移
FutureTask
的状态转移是同步操作的核心,状态转移图大致如下:
NEW -> COMPLETING -> NORMAL
| |
| v
+------> EXCEPTIONAL <------+
|
v
CANCELLED
- 从
NEW
到COMPLETING
:当任务开始执行时,状态变为COMPLETING
。 - 从
COMPLETING
到NORMAL
或EXCEPTIONAL
:任务执行完毕后,根据是否有异常,状态变为NORMAL
或EXCEPTIONAL
。 - 从
NEW
到CANCELLED
或INTERRUPTED
:任务在执行前或执行中可以被取消或中断。
5、FutureTask任务执行流程
(1)、FutureTask
通过构造函数接收一个 Callable
对象。
(2)、任务通过 run()
方法启动,该方法被线程池中的线程调用。
(3)、如果任务正常执行,执行结果通过 set()
方法设置,状态变为 NORMAL
。
(4)、如果任务执行过程中抛出异常,异常通过 setException()
方法设置,状态变为 EXCEPTIONAL
。
(5)、如果任务在执行前或执行中被取消,状态变为 CANCELLED
。
6、使用示例
import java.util.concurrent.*;
public class FutureTaskExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
FutureTask<Integer> futureTask = new FutureTask<>(() -> {
try {
// 模拟耗时操作
Thread.sleep(2000);
return 42; // 任务返回结果
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
});
// 将 FutureTask 作为 Runnable 任务提交给线程池
executor.execute(futureTask);
try {
// 获取任务结果,会阻塞直到任务完成
Integer result = futureTask.get();
System.out.println("Task result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
// 关闭线程池
executor.shutdown();
}
}
}
以下是 FutureTask
类中一些关键方法的简要说明:
protected void set(Object x)
: 设置任务执行结果,并通知所有等待的线程。protected void setException(Throwable t)
: 设置任务执行异常,并通知所有等待的线程。public boolean run()
: 如果任务尚未启动,则执行任务。public boolean cancel(boolean mayInterruptIfRunning)
: 取消任务,如果任务正在运行,可以选择中断执行线程。public Object get()
: 等待任务完成并获取结果。public Object get(long timeout, TimeUnit unit)
: 等待任务完成直到超时时间,并获取结果。
FutureTask
的实现确保了任务的执行是线程安全的,并且结果只能被设置一次。它通过内部同步机制来管理状态转换和结果访问。
三、手写一个线程池
有了对Callable、Future和FutureTask的把握,我们就可以实战一下手写线程池了。一个高效的线程池,除了基本的任务执行功能外,还需要具备以下特性:
- 线程池大小合理控制
- 线程复用和缓存队列
- 拒绝策略和异常处理
- 生命周期管理
让我们用代码一一拆解并实现上述功能:
创建一个简单的线程池需要实现多个组件和逻辑。下面是一个简化版的线程池实现。
1. 线程池框架设计
首先,定义线程池的基础结构和接口。
public class SimpleThreadPool {
private final int corePoolSize;
private final BlockingQueue<Runnable> workQueue;
private final RejectedExecutionHandler handler;
private volatile boolean isShutDown = false;
private final List<Worker> workers = new ArrayList<>();
public SimpleThreadPool(int corePoolSize, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
this.corePoolSize = corePoolSize;
this.workQueue = workQueue;
this.handler = handler;
}
// 提交任务
public void submit(Runnable task) {
if (isShutDown) {
throw new RejectedExecutionException("ThreadPool is shutdown");
}
Runnable r = task;
if (r == null) throw new NullPointerException();
if (workers.size() < corePoolSize || !workQueue.offer(r)) {
synchronized (this) {
if (isShutDown) throw new RejectedExecutionException("ThreadPool is shutdown");
// 创建并启动新线程
Worker worker = new Worker(r);
workers.add(worker);
worker.start();
}
} else {
// 任务缓存到队列
handler.rejectedExecution(r, this);
}
}
// 关闭线程池
public void shutdown() {
isShutDown = true;
}
// 钩子方法
protected void beforeExecute(Thread t, Runnable r) {
}
protected void afterExecute(Runnable r, Throwable t) {
}
// 内部工作线程
private class Worker extends Thread {
private final AtomicBoolean running = new AtomicBoolean(false);
private Runnable firstTask;
public Worker(Runnable firstTask) {
this.firstTask = firstTask;
}
public void run() {
try {
while (running.get()) {
try {
Runnable task = firstTask != null ? firstTask : workQueue.take();
firstTask = null;
beforeExecute(Thread.currentThread(), task);
task.run();
afterExecute(task, null);
} catch (InterruptedException e) {
// 线程被中断
} catch (Throwable e) {
afterExecute(null, e);
}
}
} finally {
workers.remove(this);
}
}
}
}
2. 工作线程设计
工作线程是通过继承 Thread
类并重写 run
方法来设计的,如上 Worker
类所示。
3. 任务缓存队列实现
任务缓存队列可以使用 BlockingQueue
接口实现,这里使用 LinkedBlockingQueue
。
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>();
4. 提交任务及执行流程
任务提交通过 submit
方法,它将任务添加到工作队列,如果工作线程少于核心大小,则创建新线程。
5. 线程池生命周期管理
线程池的生命周期通过 isShutDown
变量管理,shutdown
方法将此变量设置为 true
。
6. 拒绝策略实现
拒绝策略通过 RejectedExecutionHandler
接口实现,自定义拒绝行为。
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, SimpleThreadPool executor);
}
// 示例拒绝策略
public class CallerRunsPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, SimpleThreadPool executor) {
r.run();
}
}
7. 钩子方法设计
beforeExecute
和 afterExecute
是线程池执行每个任务前后的钩子方法。
8. 测试用例
public class SimpleThreadPoolTest {
public static void main(String[] args) {
SimpleThreadPool pool = new SimpleThreadPool(3, new LinkedBlockingQueue<>(), new CallerRunsPolicy());
for (int i = 0; i < 10; i++) {
int finalI = i;
pool.submit(() -> System.out.println("Task " + finalI + " executed by " + Thread.currentThread().getName()));
}
pool.shutdown();
}
}
关键逻辑和代码实现技巧
- 线程安全:使用
AtomicBoolean
和synchronized
块来确保线程安全。 - 任务队列:使用
BlockingQueue
来缓存任务,确保任务不会丢失。 - 拒绝策略:当任务无法处理时,自定义拒绝策略,例如可以运行任务、丢弃任务或抛出异常。
- 生命周期管理:通过一个变量控制线程池的状态,确保在关闭时不接受新任务。
- 钩子方法:允许在任务执行前后执行额外的操作,如日志记录或资源管理。
请注意,这是一个非常基础的线程池实现,用于教学目的。在生产环境中,建议使用 Java 内置的 java.util.concurrent
包中的 ThreadPoolExecutor
,它提供了更全面的功能和优化。
四、生产实践中的最佳实践
通过前面的理论学习和动手实战,相信大家已经比较扎实地掌握了Callable、Future以及如何手写一个线程池。不过,在生产实践中还需要注意哪些地方呢?让我结合过往的经验,分享一些最佳实践建议:
1. 线程池参数设置
核心池大小(Core Pool Size):
-
核心池大小应根据应用程序的性质和预期的工作负载来设置。例如,对于主要执行CPU密集型任务的应用程序,核心池大小应设置为等于机器的核心数,以避免过多的线程上下文切换。
-
例子:
new ThreadPoolExecutor(4, 8, ...)
- 对于一个有4个CPU核心的服务器,可以将核心池大小设置为4。
最大池大小(Maximum Pool Size):
-
最大池大小应考虑到系统资源和任务的特性。如果任务可以等待,可以适当增加最大池大小,以便在高峰时段处理更多任务。
-
例子:
new ThreadPoolExecutor(..., 16, ...)
- 对于I/O密集型任务,最大池大小可以设置得更高,比如16。
工作队列(Work Queue):
-
选择合适的工作队列类型和大小。对于有界队列,可以避免内存溢出问题,但也可能导致任务拒绝。
-
例子:
new LinkedBlockingQueue<>(100)
- 使用有界队列,大小为100,可以缓存100个待处理任务。
线程存活时间(Keep-Alive Time):
-
设置非核心线程的存活时间,以回收长时间空闲的线程。
-
例子:
new ThreadPoolExecutor(..., 1, TimeUnit.MINUTES, ...)
- 非核心线程空闲1分钟后将被终止。
线程工厂(Thread Factory):
-
使用自定义线程工厂设置有意义的线程名称,有助于日志记录和问题排查。
-
例子:
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("my-pool-%d").build();
2. 任务提交优化
批量任务提交:
- 如果有大量的任务需要提交,考虑使用批量提交而不是单独提交每个任务,以减少同步开销。
- 例子:使用
ExecutorCompletionService
来批量提交和处理任务。
优先级队列:
- 如果任务有优先级,可以使用优先级队列(
PriorityBlockingQueue
)来确保高优先级的任务先被执行。
任务依赖性:
- 对于有依赖关系的任务,可以使用
Future
对象来安排依赖任务的提交。
避免提交空任务:
- 避免提交
null
任务,这将导致线程池抛出NullPointerException
。
3. 异常处理
捕获和记录异常:
-
在任务执行时捕获并记录异常,避免任务因未捕获的异常而失败。
-
例子:
Runnable task = () -> { try { // 任务代码 } catch (Exception e) { // 记录异常 } };
自定义拒绝策略:
-
实现自定义拒绝策略来处理无法接受的任务,例如,记录日志、重试或丢弃任务。
-
例子:
ThreadPoolExecutor executor = new ThreadPoolExecutor(..., new ThreadPoolExecutor.CallerRunsPolicy());
线程中断:
-
正确处理线程中断,避免在任务执行中出现
InterruptedException
。 -
例子:
public void run() { try { // 任务代码 } catch (InterruptedException e) { // 处理中断 } finally { // 清理资源 } }
4、实战经验总结
- 监控和度量: 监控线程池的状态和性能指标,如活跃线程数、任务队列大小等。
- 资源隔离: 对于不同类型的任务,考虑使用不同的线程池,以避免资源争抢。
- 优雅关闭: 在应用程序关闭时,提供足够的时间让线程池中的线程优雅地关闭。
- 持续优化: 根据应用程序的实际运行情况,持续调整线程池参数。
通过遵循这些最佳实践,可以有效地提高线程池的性能,减少资源浪费,并确保应用程序的稳定性和可维护性。
五、结语
走过如此漫长的篇幅,我们已将关于Callable、Future和线程池实战的知识版图全景勾勒出来。然而,多线程编程之路终究还很长,比如如何利用CompletableFuture优雅地组合并发任务?又该如何解决死锁等并发难题?更多精彩等待我们一同去发现…