多线程编程之FutureTask

FutureTask是Java中的一个类,位于java.util.concurrent包中。它实现了RunnableFuture接口,同时也是RunnableFuture的子类,因此可以作为一个任务提交给线程池执行,并且可以获取任务的执行结果

下面从源码角度分析一下 FutureTask 这个类:

构造函数

FutureTask 有以下两个构造函数:

ini 复制代码
public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // 初始状态为 NEW
}

public FutureTask(Runnable runnable, V result) {
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       // 初始状态为 NEW
}

其中,第一个构造函数接收一个 Callable<V> 对象作为参数,表示要执行的异步任务;第二个构造函数接收一个 Runnable 和一个初始结果 V 作为参数,它会将 Runnable 封装成一个 Callable 对象进行执行,并返回结果。

状态转换

FutureTask 有下面这些状态:

arduino 复制代码
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;

其中,状态转换如下:

  • 初始状态为 NEW
  • 当任务执行完成时,状态会变为 COMPLETING,表示正在完成中
  • 如果任务执行成功,状态会变为 NORMAL,并保存执行结果
  • 如果任务执行异常,状态会变为 EXCEPTIONAL,并保存异常信息
  • 如果任务被取消,状态会变为 CANCELLED
  • 如果任务正在执行中,但被中断了,状态会变为 INTERRUPTING,并尝试中断任务
  • 如果任务被中断成功,状态会变为 INTERRUPTED

执行任务

FutureTask 的核心是执行任务,它会根据任务的状态来决定如何执行:

ini 复制代码
public void run() {
    if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex);
            }
            if (ran)
                set(result);
        }
    } finally {
        runner = null;
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

方法主要做了以下几件事:

  • 判断当前任务是否处于初始状态,并且使用 CAS 操作将当前线程设置为执行线程,如果设置失败,则说明已经有其他线程在执行任务了,返回
  • 获取任务的 Callable 对象,如果任务状态不是 NEW,则直接返回
  • 使用 Callable 执行任务,如果执行成功,使用 set 方法保存结果;如果执行异常,使用 setException 方法保存异常信息
  • 清除执行线程,并根据任务状态决定是否需要处理取消中断的情况

保存结果

kotlin 复制代码
protected void set(V v) {
    if (U.compareAndSwapInt(this, STATE, NEW, COMPLETING)) {
        outcome = v;
        U.putOrderedInt(this, STATE, NORMAL); // final state
        finishCompletion();
    }
}

方法主要通过原子操作进行状态的变更,如果之前是NEW,则将其通过compareAndSwapInt变为COMPLETING。然后进行结果的赋值,赋值成功后,通过putOrderedInt将状态设置为NORMAL

为什么一开始使用compareAndSwapInt,然后又使用了putOrderedInt?其实人家设计者一定有自己的考虑,这里从成本开销简单看一下:

首先compareAndSwapInt方法的成本消耗主要包括两个方面:

  1. 内存访问成本:compareAndSwapInt方法需要读取和写入内存中的数据,这涉及到CPU与内存之间的数据传输和缓存同步,因此会产生一定的内存访问成本。
  2. CPU指令成本:compareAndSwapInt方法在底层使用了CPU的CAS(Compare-And-Swap)指令来保证操作的原子性。CAS指令需要执行多条汇编指令,包括读取内存、比较值、更新值等操作,因此会产生一定的CPU指令成本。

总的来说,compareAndSwapInt方法的成本消耗相对较高,但是它提供了一种可靠地在多线程环境下进行原子操作的机制,以确保线程安全性。

然后putOrderedInt方法的成本消耗主要包括两个方面:

  1. 内存访问成本:putOrderedInt方法需要写入内存中的数据,这涉及到CPU与内存之间的数据传输和缓存同步,因此会产生一定的内存访问成本。
  2. 缓存一致性成本:putOrderedInt方法不保证立即对其他线程可见,这意味着其他线程可能无法立即看到该字段的更新。在多核处理器上,为了保持缓存一致性,需要通过内存屏障指令或者其他机制来确保对其他线程的可见性。这样的操作会引入额外的开销(例如内存屏障,就是禁止指令重排,会带来额外的处理器开销,毕竟指令重排将顺序处理下一条应该执行的命令变为处理下一条能够执行的命令,有提高处理器的能力)。

总的来说,putOrderedInt方法的成本消耗相对较低,比起compareAndSwapInt等具有原子性保证的方法,它不需要执行CAS指令,因此在性能上可能更加高效。

最后回到问题本身,在if判断中,一定要使用原子性的操作来保障判断是没有问题的,否则多线程并发问题会直接影响逻辑。在赋值的时候,不使用CAS可以节约开销,而且延迟同步的方式也在可接受范围内,毕竟COMPLETING是一个中间态,而中间态是瞬时的,其他状态都是最终态,不会再做什么变动。

获取结果

FutureTask 实现了 Future 接口,因此可以使用 get 方法获取任务执行结果:

ini 复制代码
public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
}

public V get(long timeout, TimeUnit unit)
    throws InterruptedException, ExecutionException, TimeoutException {
    if (unit == null)
        throw new NullPointerException();
    int s = state;
    if (s <= COMPLETING && (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
        throw new TimeoutException();
    return report(s);
}

其中,get 方法会一直阻塞直到任务执行完成,并返回执行结果或者异常信息;get(timeout, unit) 方法会阻塞等待一定时间,如果任务未能在指定时间内完成,会抛出 TimeoutException

再往细节上面掰扯一下:awaitDone方法

ini 复制代码
/**
 等待完成或在中断或超时时中止。 
   @param timed 如果使用定时等待,则为true
   @param nanos 如果使用定时等待,则为等待的时间
   @return 完成时的状态或超时时的状态
 */
private int awaitDone(boolean timed, long nanos)
    throws InterruptedException {
    //对于每次调用park,仅调用一次nanoTime,如果nanos <= 0L,则立即返回,无需分配或调用nanoTime,如果nanos == Long.MIN_VALUE,则不会发生下溢,如果nanos == Long.MAX_VALUE,并且nanoTime不是单调的
    long startTime = 0L;
    //这是一个等待的节点,结构中包含Thread的能力
    WaitNode q = null;
    boolean queued = false;
    //get方法阻塞,一直等待获取到完成结果或者异常状态
    for (;;) {
        int s = state;
        //如果完成了就返回值,并且置空等待节点
        if (s > COMPLETING) {
            if (q != null)
                q.thread = null;
            return s;
        }
        //如果处于中间状态,则释放CPU,再来一次,毕竟中间态是一个瞬时态
        else if (s == COMPLETING)
            Thread.yield();
        //线程被置于中断态,则抛异常
        else if (Thread.interrupted()) {
            removeWaiter(q);
            throw new InterruptedException();
        }
        //执行到这里,表示是第一个线程刚进来
        else if (q == null) {
            if (timed && nanos <= 0L)
                return s;
            q = new WaitNode();
        }
        else if (!queued)
            queued = U.compareAndSwapObject(this, WAITERS, q.next = waiters, q);
        //是否超时,超时则异常,否则在未超时时间内等待返回结果
        else if (timed) {
            final long parkNanos;
            if (startTime == 0L) { // first time
                startTime = System.nanoTime();
                if (startTime == 0L)
                    startTime = 1L;
                parkNanos = nanos;
            } else {
                long elapsed = System.nanoTime() - startTime;
                if (elapsed >= nanos) {
                    removeWaiter(q);
                    return state;
                }
                parkNanos = nanos - elapsed;
            }
            // nanoTime may be slow; recheck before parking
            if (state < COMPLETING)
                LockSupport.parkNanos(this, parkNanos);
        }
        else
            LockSupport.park(this);
    }
}

再看一下结果是如何返回的:report方法,逻辑比较简单,就是根据状态返回不同的值

java 复制代码
private V report(int s) throws ExecutionException {
    Object x = outcome;
    if (s == NORMAL)
        return (V)x;
    if (s >= CANCELLED)
        throw new CancellationException();
    throw new ExecutionException((Throwable)x);
}

取消任务

FutureTask 还实现了取消任务的方法:

java 复制代码
public boolean cancel(boolean mayInterruptIfRunning) {
    if (!(state == NEW && UNSAFE.compareAndSwapInt(this, stateOffset, NEW, mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
        return false;
    try {    
    // in case call to interrupt throws exception
        if (mayInterruptIfRunning) {
            try {
                Thread t = runner;
                if (t != null)
                    t.interrupt();
            } finally { // final state
                UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
            }
        }
    } finally {
        finishCompletion();
    }
    return true;
}

如果任务当前状态是 NEW,它会使用 CAS 操作将任务状态改为 INTERRUPTINGCANCELLED,并根据 mayInterruptIfRunning 参数决定是否要中断正在执行的任务。如果成功取消了任务,会调用 finishCompletion 方法来完成任务的清理工作。

总结

通过以上的分析,我们可以看出,FutureTask 是一个非常强大、灵活、可扩展的类,它不仅可以用于异步执行任务和获取结果,还支持任务取消、超时等功能,并且使用了很多并发编程中常用的技巧和设计模式。FutureTask 的源码实现比较简洁、易于理解

相关推荐
oi7713 分钟前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器
工业甲酰苯胺20 分钟前
MySQL 主从复制之多线程复制
android·mysql·adb
少说多做34332 分钟前
Android 不同情况下使用 runOnUiThread
android·java
知兀32 分钟前
Java的方法、基本和引用数据类型
java·笔记·黑马程序员
蓝黑20201 小时前
IntelliJ IDEA常用快捷键
java·ide·intellij-idea
Ysjt | 深1 小时前
C++多线程编程入门教程(优质版)
java·开发语言·jvm·c++
shuangrenlong1 小时前
slice介绍slice查看器
java·ubuntu
牧竹子1 小时前
对原jar包解压后修改原class文件后重新打包为jar
java·jar
数据小爬虫@1 小时前
如何利用java爬虫获得淘宝商品评论
java·开发语言·爬虫