深入理解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,还是将其放入队列中等待执行,或者拒绝执行(如果队列已满且线程数达到最大线程数)。这有助于提高系统的整体性能和资源利用率。
相关文章
|
12天前
|
存储 Java 关系型数据库
高效连接之道:Java连接池原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。频繁创建和关闭连接会消耗大量资源,导致性能瓶颈。为此,Java连接池技术通过复用连接,实现高效、稳定的数据库连接管理。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接池的基本操作、配置和使用方法,以及在电商应用中的具体应用示例。
32 5
|
2天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
2天前
|
Java
Java之CountDownLatch原理浅析
本文介绍了Java并发工具类`CountDownLatch`的使用方法、原理及其与`Thread.join()`的区别。`CountDownLatch`通过构造函数接收一个整数参数作为计数器,调用`countDown`方法减少计数,`await`方法会阻塞当前线程,直到计数为零。文章还详细解析了其内部机制,包括初始化、`countDown`和`await`方法的工作原理,并给出了一个游戏加载场景的示例代码。
Java之CountDownLatch原理浅析
|
4天前
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
Java ArrayList扩容的原理
|
10天前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
26 2
|
13天前
|
Java 数据格式 索引
使用 Java 字节码工具检查类文件完整性的原理是什么
Java字节码工具通过解析和分析类文件的字节码,检查其结构和内容是否符合Java虚拟机规范,确保类文件的完整性和合法性,防止恶意代码或损坏的类文件影响程序运行。
|
10天前
|
算法 Java 数据库连接
Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性
本文详细介绍了Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性。连接池通过复用数据库连接,显著提升了应用的性能和稳定性。文章还展示了使用HikariCP连接池的示例代码,帮助读者更好地理解和应用这一技术。
25 1
|
存储 Java
【Java 虚拟机原理】线程栈 | 栈帧 | 局部变量表 | 反汇编字节码文件 | Java 虚拟机指令手册 | 程序计数器
【Java 虚拟机原理】线程栈 | 栈帧 | 局部变量表 | 反汇编字节码文件 | Java 虚拟机指令手册 | 程序计数器
126 0
【Java 虚拟机原理】线程栈 | 栈帧 | 局部变量表 | 反汇编字节码文件 | Java 虚拟机指令手册 | 程序计数器
|
2天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
11天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
下一篇
无影云桌面