深入理解Java中的FutureTask:用法和原理

简介: 【10月更文挑战第28天】`FutureTask` 是 Java 中 `java.util.concurrent` 包下的一个类,实现了 `RunnableFuture` 接口,支持异步计算和结果获取。它可以作为 `Runnable` 被线程执行,同时通过 `Future` 接口获取计算结果。`FutureTask` 可以基于 `Callable` 或 `Runnable` 创建,常用于多线程环境中执行耗时任务,避免阻塞主线程。任务结果可通过 `get` 方法获取,支持阻塞和非阻塞方式。内部使用 AQS 实现同步机制,确保线程安全。

一、FutureTask 概述


1. 定义


  • FutureTask是 Java 中的一个类,位于java.util.concurrent包中,它实现了RunnableFuture接口,而RunnableFuture接口又同时继承了RunnableFuture接口。这意味着FutureTask既可以作为一个Runnable被线程执行,又可以作为一个Future来获取异步计算的结果。


2. 作用


  • 异步计算:在多线程编程中,FutureTask用于封装一个可调用任务(例如实现了Callable接口的任务),并允许在一个单独的线程中执行该任务。这样可以在执行耗时操作(如网络请求、文件读取、复杂计算等)时,不会阻塞主线程或其他线程的执行。
  • 结果获取:提供了一种机制来获取异步计算的结果。通过FutureTaskget方法,可以在任务完成后获取其执行结果,如果任务尚未完成,get方法可以阻塞当前线程,直到任务完成并返回结果。


二、FutureTask 用法


1. 创建 FutureTask


  • 基于 Callable 接口
  • 首先,需要创建一个实现Callable接口的类。Callable接口与Runnable接口类似,但它可以返回一个结果并且可以抛出异常。例如:


import java.util.concurrent.Callable;
// 定义一个Callable任务,用于计算两个数的和
class AddTask implements Callable<Integer> {
    private int num1;
    private int num2;
    public AddTask(int num1, int num2) {
        this.num1 = num1;
        this.num2 = num2;
    }
    @Override
    public Integer call() throws Exception {
        return num1 + num2;
    }
}


  • 然后,使用这个Callable任务创建一个FutureTask对象:


import java.util.concurrent.FutureTask;
// 创建FutureTask对象
FutureTask<Integer> futureTask = new FutureTask<>(new AddTask(3, 5));


  • 基于 Runnable 接口和结果生成器(不常用)
  • 也可以基于Runnable接口创建FutureTask,但需要额外提供一个结果生成器(Callable)来定义任务的结果。这种方式相对复杂,不常用。例如:


import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicInteger;
// 定义一个Runnable任务,用于简单的计数
class CounterRunnable implements Runnable {
    private AtomicInteger count = new AtomicInteger(0);
    @Override
    public void run() {
        count.incrementAndGet();
    }
}
// 定义一个Callable任务,用于获取计数结果
class CounterResultCallable implements Callable<Integer> {
    private CounterRunnable counterRunnable;
    public CounterResultCallable(CounterRunnable counterRunnable) {
        this.counterRunnable = counterRunnable;
    }
    @Override
    public Integer call() throws Exception {
        return counterRunnable.count.get();
    }
}
// 创建基于Runnable和结果生成器的FutureTask
CounterRunnable counterRunnable = new CounterRunnable();
FutureTask<Integer> futureTaskFromRunnable = new FutureTask<>(counterRunnable, new CounterResultCallable(counterRunnable));


2. 执行 FutureTask


  • 提交给线程池执行
  • 通常会将FutureTask提交给线程池来执行,这样可以更好地管理线程资源。例如,使用ExecutorService线程池:


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// 创建一个固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(1);
// 提交FutureTask到线程池执行
executorService.submit(futureTask);
// 关闭线程池(注意:这里的关闭方式不会立即终止正在执行的任务)
executorService.shutdown();


  • 直接在单独线程中执行
  • 也可以直接在一个单独的线程中执行FutureTask,不过这种方式不太灵活,且不利于线程资源的管理:


import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ThreadPoolExecutor;
// 创建一个FutureTask
FutureTask<Integer> futureTask = new FutureTask<>(new AddTask(3, 5));
// 创建一个线程并执行FutureTask
Thread thread = new Thread(futureTask);
thread.start();


3. 获取结果


  • 阻塞式获取结果
  • 使用FutureTaskget方法可以获取任务的结果。如果任务还未完成,调用get方法的线程会被阻塞,直到任务完成并返回结果。例如:


import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
try {
    // 获取FutureTask的结果,可能会阻塞
    Integer result = futureTask.get();
    System.out.println("结果是: " + result);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}


  • 非阻塞式检查结果
  • 通过FutureTaskisDone方法,可以在不阻塞的情况下检查任务是否已经完成。例如:


if (futureTask.isDone()) {
    try {
        Integer result = futureTask.get();
        System.out.println("结果是: " + result);
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
} else {
    System.out.println("任务尚未完成");
}


三、FutureTask 原理


1. 内部状态


  • 状态变量FutureTask内部使用一个volatile修饰的整数变量来表示状态,这个变量有不同的取值,对应不同的任务状态,如新建(NEW)、已完成(COMPLETED)、已取消(CANCELLED)等。这些状态的转换是原子操作,通过Unsafe类或CAS(Compare - and - Swap)机制来保证线程安全。
  • 状态转换:例如,当任务开始执行时,状态从NEW转换为RUNNING,当任务执行成功完成后,状态转换为COMPLETED,如果任务被取消,状态转换为CANCELLEDINTERRUPTED(取决于取消的方式)。


2. 实现机制


  • 基于 AQS(AbstractQueuedSynchronizer)的同步机制FutureTask的底层实现依赖于AQS来实现同步和阻塞。AQS是一个用于构建锁和同步器的框架,它提供了基于队列的等待和唤醒机制。FutureTask通过继承AQS来实现自己的同步逻辑。
  • 等待获取结果:当一个线程调用FutureTaskget方法时,如果任务尚未完成,该线程会被封装成一个Node添加到AQS的等待队列中,然后线程会被阻塞。这个等待队列是一个双向链表结构,用于管理等待获取结果的线程。
  • 任务完成后的唤醒:当任务完成后,FutureTask会通过AQS的唤醒机制,将等待队列中的线程逐个唤醒。唤醒的线程会再次尝试获取任务的结果,如果任务已经完成,就可以成功获取结果,否则会再次被阻塞。
  • 结果存储和可见性:任务的结果存储在FutureTask内部的一个变量中,通过volatile修饰来保证结果的内存可见性。当任务完成后,结果会被正确地写入这个变量,并且其他等待获取结果的线程可以立即看到这个结果。


3. 与线程池的协作


  • 线程池中的任务调度:当FutureTask被提交给线程池(如ExecutorService)时,线程池会从自己的工作队列中取出FutureTask并分配给一个空闲的线程来执行。线程池中的线程在执行FutureTask时,与直接执行FutureTask的原理是一样的,都是通过AQS来实现同步和阻塞,以及通过状态转换来管理任务的执行过程。
  • 线程池的资源管理和优化:线程池可以根据自身的配置和当前的负载情况,合理地分配资源来执行FutureTask。例如,一个ThreadPoolExecutor可以根据核心线程数、最大线程数、队列容量等参数来决定是立即执行FutureTask,还是将其放入队列中等待执行,或者拒绝执行(如果队列已满且线程数达到最大线程数)。这有助于提高系统的整体性能和资源利用率。
相关文章
|
7天前
|
存储 算法 Java
Java HashSet:底层工作原理与实现机制
本文介绍了Java中HashSet的工作原理,包括其基于HashMap实现的底层机制。通过示例代码展示了HashSet如何添加元素,并解析了add方法的具体过程,包括计算hash值、处理碰撞及扩容机制。
|
6天前
|
开发框架 Java 程序员
揭开Java反射的神秘面纱:从原理到实战应用!
本文介绍了Java反射的基本概念、原理及应用场景。反射允许程序在运行时动态获取类的信息并操作其属性和方法,广泛应用于开发框架、动态代理和自定义注解等领域。通过反射,可以实现更灵活的代码设计,但也需注意其性能开销。
22 1
|
18天前
|
算法 Java 开发者
Java中的垃圾回收机制:从原理到实践
Java的垃圾回收机制(Garbage Collection, GC)是其语言设计中的一大亮点,它为开发者提供了自动内存管理的功能,大大减少了内存泄漏和指针错误等问题。本文将深入探讨Java GC的工作原理、不同垃圾收集器的种类及它们各自的优缺点,并结合实际案例展示如何调优Java应用的垃圾回收性能,旨在帮助读者更好地理解和有效利用Java的这一特性。
|
19天前
|
前端开发 Java API
JAVA Web 服务及底层框架原理
【10月更文挑战第1天】Java Web 服务是基于 Java 编程语言用于开发分布式网络应用程序的一种技术。它通常运行在 Web 服务器上,并通过 HTTP 协议与客户端进行通信。
15 1
|
17天前
|
IDE Java 编译器
java反射机制原理
java反射机制原理
47 0
|
存储 Java
【Java 虚拟机原理】线程栈 | 栈帧 | 局部变量表 | 反汇编字节码文件 | Java 虚拟机指令手册 | 程序计数器
【Java 虚拟机原理】线程栈 | 栈帧 | 局部变量表 | 反汇编字节码文件 | Java 虚拟机指令手册 | 程序计数器
126 0
【Java 虚拟机原理】线程栈 | 栈帧 | 局部变量表 | 反汇编字节码文件 | Java 虚拟机指令手册 | 程序计数器
|
2天前
|
Java API 调度
Java中的多线程编程:理解与实践
本文旨在为读者提供对Java多线程编程的深入理解,包括其基本概念、实现方式以及常见问题的解决方案。通过阅读本文,读者将能够掌握Java多线程编程的核心知识,提高自己在并发编程方面的技能。
|
1天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
7 3
|
1天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
7 2
|
1天前
|
Java
在Java多线程编程中,实现Runnable接口通常优于继承Thread类
【10月更文挑战第20天】在Java多线程编程中,实现Runnable接口通常优于继承Thread类。原因包括:1) Java只支持单继承,实现接口不受此限制;2) Runnable接口便于代码复用和线程池管理;3) 分离任务与线程,提高灵活性。因此,实现Runnable接口是更佳选择。
8 2