FutureTask源码解析

FutureTask继承体系

Runnable和Callable是多线程中的两个任务接口,实现接口的类将拥有多线程的功能,FutureTask类与这两个类是息息相关!

FutureTask的构造方法

构造方法1 接收Callable对象

ini 复制代码
public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
}

构造方法2 接收Runnable对象和一个泛型的result

ini 复制代码
public FutureTask(Runnable runnable, V result) {
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       // ensure visibility of callable
}

原来,FutureTask内部维护Callable类型的成员变量,对于Callable任务,直接赋值即可。而对于Runnable任务,需要先调用Executors.callable()把Runnable先包装成Callable。

ini 复制代码
Executors.callable(runnable, result);

这行代码用了适配器模式,你给我一个runnable对象,我还你一个callable对象。

typescript 复制代码
public static <T> Callable<T> callable(Runnable task, T result) {
    if (task == null)
        throw new NullPointerException();
    return new Executors.RunnableAdapter<T>(task, result);
}

RunnableAdapter是Executors中的静态内部类,上面代码意思是调用该静态内部类的构造方法,生成RunnableAdapter 对象,而RunnableAdapter对象实现了Callable接口,根据多态也就相当于得到了一个Callable对象。

kotlin 复制代码
static final class RunnableAdapter<T> implements Callable<T> {
    final Runnable task;
    final T result;
    RunnableAdapter(Runnable task, T result) {
        this.task = task;
        this.result = result;
    }
    public T call() {
        task.run();
        return result;
    }
}

unnableAdapter作为Callable的适配器,也拥有call方法,这就是适配器模式。

如果你是用第二种方式来构造FutureTask对象,因为传入的是Runnable,Runnable的run方法是没有返回值的,而Callable的call方法是有返回值的,所以这边就折中一下,返回值需要你在构建FutureTask对象时自己传进去,最后再原封不动地还给你。

如果你是用第一种方式来构造FutureTask对象,那就简单多了,直接传入一个Callable对象即可,返回值你自己决定。

为什么要用FutureTask?

多线程是Java进阶的难点,也是面试的重灾区,请确保你把上面的代码都理解了之后再来看这一节。

我们再回过头来想想,如何使用多线程呢,是不是有3个方法?如果记不得了请回过去看看上一个章节【线程类】。

第1种方法是直接继承Thread类,重写run方法。

第2种方法是实现Runnable接口,然后还是要靠Thread类的构造器,把Runnable传进去,最终调用的就是Runnable的run方法。

第3种方法是用线程池技术,用ExecutorService去提交Runnable对象/Callable对象,区别是Runnable没有返回值,Callable对象有返回值。

你发现没有,不管你用哪种方式,最终都是要靠Thread类去开启线程的。因为,有且仅有Thread类能通过start0()方法向操作系统申请线程资源(本地方法)

第一种方法因为耦合性太高,很少会使用,实际开发中我们一般都会使用线程池技术,所以第3种方法是有实战意义的。那么问题来了,Runnable和Callable对象都可以被用作线程池的任务,就有人会乱用了啊,有的人喜欢Runnable,有的喜欢Callable,到时候项目的代码就乱成一锅粥啦!

所以,我私以为Java的创始人意识到这一点,就干脆搞一个FutureTask出来一统江湖。我说的这么白,应该都明白了吧,嘿嘿。

FutureTask的7种状态

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

状态含义分别是:

● 0-刚创建

● 1-计算中

● 2-完成

● 3-抛异常

● 4-任务取消

● 5-任务即将被打断

● 6-任务被打断

为什么要设置这些状态呢,那是因为FutureTask=任务+结果,调用者何时可以去获取这个结果result呢?FutureTask在调用get方法时,会去判断当前任务的状态,只有当任务完成才会给你实际的result,因此get方法是阻塞的。

FutureTask的get() 方法

都是调用awaitDone方法

ini 复制代码
private int awaitDone(Boolean timed, long nanos)
									throws InterruptedException {
//如果设置了超时时间timed=true,那么deadline就是超时时间,超过就超时了
	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;
		}
//如果执行状态是正在执行,说明任务已经完成.那么现在需要给其他正在执行的任务让路,挂起线程.
		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);
	}
}

触发流程:

1.第一轮for循环,执行逻辑q == null,新建等待节点q,循环结束

2.第二轮for循环,执行!q,入队,循环结束.

3.第三轮for循环,进行阻塞等待或者阻塞特定时间,直到阻塞被其他线程唤醒.

4.唤醒后第四轮for循环,根据前三个条件进入对应的逻辑中

finishCompletion

该方法主要用于唤醒线程.当任务结束或者异常时,会调用该方法

被唤醒的线程就会从awaitDown方法中的LockSupport的park或者parkNanos方法处唤醒,然后继续执行awaitDown方法

ini 复制代码
private void finishCompletion() {
        // 遍历等待节点
        for (WaitNode q; (q = waiters) != null;) {
            if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
                for (;;) {
                    Thread t = q.thread;
                    if (t != null) {
                        q.thread = null;
								// 唤醒等待线程 
                        LockSupport.unpark(t);
                    }
                    WaitNode next = q.next;
                    if (next == null)
                        break;
                    q.next = null; // unlink to help gc
                    q = next;
                }
                break;
            }
        }

        done();

        callable = null;        // to reduce footprint
    }

FutureTask的run方法

最终是存储到outcome对象了,简而言之,FutureTask的run方法的作用就是运行callable的call方法,拿到返回值保存到outcome对象,等待有人来取。

参考文章

1.浅谈Java多线程之FutureTask - 剽悍一小兔 - 博客园 (cnblogs.com)

2.FutureTask源码 - get方法解析 - 掘金 (juejin.cn)

相关推荐
ltl7 小时前
Transformer 原论文实验结果:为什么 28.4 BLEU 足以改写路线图
后端
excel7 小时前
为什么我推荐使用 Termius:现代 SSH 工具的完整体验
前端·后端
卷毛的技术笔记8 小时前
Java后端硬核实战:用Spring AI Alibaba+Redis给LLM装上“超强记忆中枢”
java·人工智能·redis·后端·spring·ai·系统架构
IT_陈寒9 小时前
Java的Optional差点让我掉坑里,这几个坑你别踩
前端·人工智能·后端
子兮曰9 小时前
Harness 驾驭工程深度教程:从 AGENTS.md 到全链路 AI 编码基础设施
前端·后端·ai编程
小杍随笔10 小时前
【Rust 工具链管理工具再升级!rust-verse v1.3.1 ~ v1.3.5 最新更新深度解析】
开发语言·后端·rust
百珏10 小时前
海量人群包存储优化:基于 RoaringBitmap 交换格式与 Redis 分片 Bitmap 的实践
java·后端·架构
叫我少年10 小时前
C# 类型系统
后端
五月君_11 小时前
Rust 重写 AI 味太浓,Bun 被 yt-dlp 封版本、Electrobun 直接解绑
开发语言·后端·rust
叫我少年11 小时前
C# 预处理器指令 — 条件编译、文件应用指令与警告控制
后端