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)

相关推荐
NiNg_1_2341 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
Chrikk2 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*2 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue2 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man2 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
customer084 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
Yaml45 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
小码编匠6 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#