简谈java并发FutureTask的实现

概述

在使用java多线程解决问题的时候,为了提高效率,我们常常会异步处理一些计算任务并在最后异步的获取计算结果,这个过程的实现离不开Future接口及其实现类FutureTask。FutureTask类实现了Runnable, Future接口,接下来我会通过源码对该类的实现进行详解。

使用

我们先看下FutureTask中的主要方法如下,可以看出FutureTask实现了任务及异步结果的集合功能。看到这块的方法,大家肯定会有疑问,Runnable任务的run方法返回空,FutureTask如何依靠该方法获取线程异步执行结果,这个问题,我们在下面给大家介绍。

//以下五个方法实现接口Future中方法
public boolean isCancelled();
public boolean isDone();
public boolean cancel();
public V get() throws InterruptedException, ExecutionException;
public V get(long timeout, TimeUnit unit);
//实现接口Runnable中方法
public void run();

我们在使用中会构造一个FutureTask对象,然后将FutureTask扔到另一个线程中执行,而主线程继续执行其他业务逻辑,一段时间后主线程调用FutureTask的get方法获取执行结果。下面我们看一个简单的例子:

/**
* Created by yuanqiongqiong on 2019/4/9.
*/
public class FutureTaskTest {
private static ExecutorService executorService = Executors.newFixedThreadPool(1);
public static void main(String []args) {
Callable callable = new AccCallable(1, 2);
FutureTask futureTask = new FutureTask(callable);
executorService.execute(futureTask);
System.out.println("go to do other things in main thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("go back in main thread");
try {
int result = (int) futureTask.get();
System.out.println("result is " + result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
static class AccCallable implements Callable<Integer> {
private int a;
private int b;
public AccCallable(int a, int b) {
this.a = a;
this.b = b;
}
@Override
public Integer call() throws Exception {
System.out.println("acc a and b in threadId = " + Thread.currentThread().getName());
return a + b;
}
}
}

输出结果为:

go to do other things in main thread
acc a and b in threadId = pool-1-thread-1
go back in main thread
result is 3

实现分析

在分析实现前,我们先想下如果让我们实现一个类似FutureTask的功能,我们会如何做?因为需要获取执行结果,需要一个Object对象来存执行结果。任务执行时间不可控性,我们需要一个变量表示执行状态。其他线程会调用get方法获取结果,在没达到超时的时候需要将线程阻塞或挂起。

因此需要一个队列类似的结构存储等待该结果的线程信息,这样在任务执行线程完成后就可以唤醒这些阻塞或挂起的线程,得到结果。FutureTask的实际实现也是类似的逻辑,具体如下。

首先看下FutureTask的主要成员变量如下:

//futureTask执行状态
private volatile int state;
//具体的执行任务,会在run方法中抵用callable.call()
private Callable<V> callable;
//执行结果
private Object outcome;
//获取结果的等待线程节点
private volatile WaitNode waiters;

对于执行状态,在源码中已经有了非常清晰的解释,这里我只是贴出源码,不在进行说明,具体如下:

/**
* Possible state transitions:
* NEW -> COMPLETING -> NORMAL
* NEW -> COMPLETING -> EXCEPTIONAL
* NEW -> CANCELLED
* NEW -> INTERRUPTING -> INTERRUPTED
*/
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;

然后我们看下FutureTask的构造函数,如下:

public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
public FutureTask(Runnable runnable, V result) {
//构造函数传入runnable对象时调用静态工具类Executors的方法转换为一个callable对象
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}

如前所述,FutureTask的执行线程中会调用其run()方法执行任务,我们看下这块逻辑:

public void run() {
//1.如果执行状态不是NEW或者有其他线程执行该任务,直接返回
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
//2.如果执行状态是NEW,即任务还没执行,直接调用callable.call()方法获取执行结果
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
//3.发生异常,更新status为EXCEPTIONAL,唤醒挂起线程
setException(ex);
}
//4.如果结果成功返回,调用set方法将设置outcome,更改status执行状态,唤醒挂起线程
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}

我们看下set函数的实现,具体看下4中的执行:

protected void set(V v) {
//将执行状态变更为COMPLETING
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
//设置执行结果
outcome = v;
//设置执行状态为NORMAL
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
//执行完成后处理操作,具体就是遍历阻塞链表,删除链表节点,并唤醒每个节点关联的线程
finishCompletion();
}
}

以上就是任务执行线程做的逻辑,以上逻辑也回答了FutureTask如何得到执行结果的疑问。下面我们看下用户调用get方法获取执行结果时的实现逻辑,这个时候FutureTask可能处理各种状态,即可能没有执行,执行中,已完成,发生异常等,具体如下:

public V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
if (unit == null)
throw new NullPointerException();
int s = state;
//执行状态是NEW或者COMPLETING时执行awaitDone将线程加入等待队列中并挂起线程
if (s <= COMPLETING &&
(s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
throw new TimeoutException();
//根据执行状态status进行结果封装
return report(s);
}
//我理解这块是get的核心逻辑
private int awaitDone(boolean timed, long nanos)
throws InterruptedException {
//如果设置了超时时间,计算还有多长时间超时
final long deadline = timed ? System.nanoTime() + nanos : 0L;
WaitNode q = null;
boolean queued = false;
for (;;) {
//如果当前线程被中断,删除等待队列中的节点,并抛出异常
if (Thread.interrupted()) {
removeWaiter(q);
throw new InterruptedException();
}
int s = state;
//如果执行状态已经完成或者发生异常,直接跳出自旋返回
if (s > COMPLETING) {
if (q != null)
q.thread = null;
return s;
}
//如果执行状态是正在执行,说明线程已经被加入到等待队列中,放弃cpu进入下次循环(真正的自旋)
else if (s == COMPLETING) // cannot time out yet
Thread.yield();
//第一次进入循环,创建节点
else if (q == null)
q = new WaitNode();
//将节点加入到等待队列中,waiters相当于头阶段,不断将头结点更新为新节点
else if (!queued)
queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
q.next = waiters, q);
else if (timed) {
//如果设置了超时时间,在进行下次循环前查看是否已经超时,如果超时删除该节点进行返回
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
removeWaiter(q);
return state;
}
//挂起当前节点
LockSupport.parkNanos(this, nanos);
}
else
LockSupport.park(this);
}
}

这里需要说明一点,FutureTask中的阻塞队列新加入的节点都在头结点并且next指向之前的头结点,waitars指针总是指向新加入节点,通过waitars可以遍历整个等待队列,具体截图如下。此外等待队列节点结构很简单成员变量只有线程引用和next指针,这里再列出器接口。

futureTask等待队列

读到这里,相信大家已经对FutureTask的实现细节有了一定的认识。此外,FutureTask没有使用锁而是使用Unsafe的是CAS的原子操作来解决竞争问题,减少了锁带来的上下文切换的开销,提高了效率。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • futuretask用法及使用场景介绍

    FutureTask可用于异步获取执行结果或取消执行任务的场景.通过传入Runnable或者Callable的任务给FutureTask,直接调用其run方法或者放入线程池执行,之后可以在外部通过FutureTask的get方法异步获取执行结果,因此,FutureTask非常适合用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果.另外,FutureTask还可以确保即使调用了多次run方法,它都只会执行一次Runnable或者Callable任务,或者通过cancel取消FutureTa

  • Java线程池FutureTask实现原理详解

    前言 线程池可以并发执行多个任务,有些时候,我们可能想要跟踪任务的执行结果,甚至在一定时间内,如果任务没有执行完成,我们可能还想要取消任务的执行,为了支持这一特性,ThreadPoolExecutor提供了 FutureTask 用于追踪任务的执行和取消.本篇介绍FutureTask的实现原理. 类视图 为了更好的理解FutureTask的实现原理,这里先提供几个重要接口和类的结构,如下图所示: RunnableAdapter ThreadPoolExecutor提供了submit接口用于提交任

  • futuretask源码分析(推荐)

    FutureTask只实现RunnableFuture接口: 该接口继承了java.lang.Runnable和Future接口,也就是继承了这两个接口的特性. 1.可以不必直接继承Thread来生成子类,只要实现run方法,且把实例传入到Thread构造函数,Thread就可以执行该实例的run方法了( Thread(Runnable) ). 2.可以让任务独立执行,get获取任务执行结果时,可以阻塞直至执行结果完成.也可以中断执行,判断执行状态等. FutureTask是一个支持取消行为的异

  • java多线程编程同步器Future和FutureTask解析及代码示例

    publicinterfaceFuture<V>Future表示异步计算的结果.它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果.计算完成后只能使用get方法来获取结果,如有必要,计算完成前可以阻塞此方法.取消则由cancel方法来执行.还提供了其他方法,以确定任务是正常完成还是被取消了.一旦计算完成,就不能再取消计算.如果为了可取消性而使用Future但又不提供可用的结果,则可以声明Future<?>形式类型.并返回null作为底层任务的结果. Future主要

  • 比较java中Future与FutureTask之间的关系

    Future与FutureTask都是用于获取线程执行的返回结果.下面我们就对两者之间的关系与使用进行一个大致的介绍与分析 一.Future与FutureTask介绍: Future位于java.util.concurrent包下,它是一个接口 public interface Future<V> { boolean cancel(boolean mayInterruptIfRunning); boolean isCancelled(); boolean isDone(); V get() t

  • 简谈java并发FutureTask的实现

    概述 在使用java多线程解决问题的时候,为了提高效率,我们常常会异步处理一些计算任务并在最后异步的获取计算结果,这个过程的实现离不开Future接口及其实现类FutureTask.FutureTask类实现了Runnable, Future接口,接下来我会通过源码对该类的实现进行详解. 使用 我们先看下FutureTask中的主要方法如下,可以看出FutureTask实现了任务及异步结果的集合功能.看到这块的方法,大家肯定会有疑问,Runnable任务的run方法返回空,FutureTask如

  • 浅谈Java并发编程之Lock锁和条件变量

    简单使用Lock锁 Java 5中引入了新的锁机制--java.util.concurrent.locks中的显式的互斥锁:Lock接口,它提供了比synchronized更加广泛的锁定操作.Lock接口有3个实现它的类:ReentrantLock.ReetrantReadWriteLock.ReadLock和ReetrantReadWriteLock.WriteLock,即重入锁.读锁和写锁.lock必须被显式地创建.锁定和释放,为了可以使用更多的功能,一般用ReentrantLock为其实例

  • 浅谈Java并发中ReentrantLock锁应该怎么用

    目录 1.重入锁 说明 2.中断响应 说明 3.锁申请等待限时 tryLock(long, TimeUnit) tryLock() 4.公平锁 说明 源码(JDK8) 重入锁可以替代关键字 synchronized . 在 JDK5.0 的早期版本中,重入锁的性能远远优于关键字 synchronized , 但从 JDK6.0 开始, JDK 在关键字 synchronized 上做了大量的优化,使得两者的性能差距并不大. 重入锁使用 ReentrantLock 实现 1.重入锁 package

  • 浅谈Java并发编程基础知识

    进程和线程 在并行程序中进程和线程是两个基本的运行单元,在Java并发编程中,并发主要核心在于线程 1. 进程 一个进程有其专属的运行环境,一个进程通常有一套完整.私有的运行时资源:尤其是每个进程都有其专属的内存空间. 通常情况下,进程等同于运行的程序或者应用,然而很多情况下用户看到的一个应用实际上可能是多个进程协作的.为了达到进程通信的目的,主要的操作系统都实现了Inter Process Communication(IPC)资源,例如pipe和sockets,IPC不仅能支持同一个系统中的进

  • 浅谈Java 并发的底层实现

    并发编程的目的是让程序运行更快,但是使用并发并不定会使得程序运行更快,只有当程序的并发数量达到一定的量级的时候才能体现并发编程的优势.所以谈并发编程在高并发量的时候才有意义.虽然目前还没有开发过高并发量的程序,但是学习并发是为了更好理解一些分布式架构.那么当程序的并发量不高,比如是单线程的程序,单线程的执行效率反而比多线程更高.这又是为什么呢?熟悉操作系统的应该知道,CPU是通过给每个线程分配时间片的方式实现多线程的.这样,当CPU从一个任务切换到另一个任务的时候,会保存上一个任务的状态,当执行

  • 浅谈Java并发中的内存模型

    什么是JavaMemoryModel(JMM)? JMM通过构建一个统一的内存模型来屏蔽掉不同硬件平台和不同操作系统之间的差异,让Java开发者无需关注不同平台之间的差异,达到一次编译,随处运行的目的,这也正是Java的设计目的之一. CPU和内存 在讲JMM之前,我想先和大家聊聊硬件层面的东西.大家应该都知道执行运算操作的CPU本身是不具备存储能力的,它只负责根据指令对传递进来的数据做相应的运算,而数据存储这一任务则交给内存去完成.虽然内存的运行速度虽然比起硬盘快非常多,但是和3GHZ,4GH

  • 浅谈Java并发 J.U.C之AQS:CLH同步队列

    CLH同步队列是一个FIFO双向队列,AQS依赖它来完成同步状态的管理,当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态. 在CLH同步队列中,一个节点表示一个线程,它保存着线程的引用(thread).状态(waitStatus).前驱节点(prev).后继节点(next),其定义如下: static final class Node

  • java并发编程专题(四)----浅谈(JUC)Lock锁

    首先我们来回忆一下上一节讲过的synchronized关键字,该关键字用于给代码段或方法加锁,使得某一时刻它修饰的方法或代码段只能被一个线程访问.那么试想,当我们遇到这样的情况:当synchronized修饰的方法或代码段因为某种原因(IO异常或是sleep方法)被阻塞了,但是锁有没有被释放,那么其他线程除了等待以外什么事都做不了.当我们遇到这种情况该怎么办呢?我们今天讲到的Lock锁将有机会为此行使他的职责. 1.为什么需要Lock synchronized 是Java 语言层面的,是内置的关

  • Java并发编程之浅谈ReentrantLock

    一.首先看图 二.lock()跟踪源码 这里对公平锁和非公平锁做了不同实现,由构造方法参数决定是否公平. public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); } 2.1 非公平锁实现 static final class NonfairSync extends Sync { private static final long serialVersionUID = 731615

  • Java多线程并发FutureTask使用详解

    目录 基本使用 代码分析 继承关系 Future RunnableFuture FutureTask 状态 属性 内部类 构造方法 检索 FutureTask 状态 取消操作 计算结果 立刻获取结果或异常 run 方法组 本文基于最新的 OpenJDK 代码,预计发行版本为 19 . Java 的多线程机制本质上能够完成两件事情,异步计算和并发.并发问题通过解决线程安全的一系列 API 来解决:而异步计算,常见的使用是 Runnable 和 Callable 配合线程使用. FutureTask

随机推荐