FutureTask的使用和源码分析

        最近在学习线程池相关的内容,关于并发编程的内容我打算把自己学到的都记录下来,本来个人是懒的记录的,可惜的是学过一遍,当时感觉自己很牛逼,都记住了,但是过了两三个月就啥到不知道了,所以这次再次学习并发编程会记录一些学习的心得,省的下次忘记了。

        关于FutureTask可以让一个线程能够拿到其他线程未来某一时刻执行的结果,然后根据结果进一步处理,下面先看一下FutureTask的继承关系。

                                           

         关于Runnable接口我们都很熟悉了,实现Runnable接口的类可以把它当做一个任务让线程来执行,当我们调用线程Thread类的start方法时,最后会调用native本地方法,主要做了以下几步:

          一:检查线程状态,确保是未启动状态,如果验证通过会将java内的线程对象Thread和操作系统级别的线程进行绑定

          二:调用java线程对象Thread类里面的run方法

         对于Thread类的run方法,最终会调用其内部Runable属性的run方法,所以,对于Runable接口的实现类,一定要清楚的认识到,它相当于任务,在线程启动后进行调用。

        下面主要来看一下Future接口,

public interface Future<V> {
    /*
    * 提供了中断/取消线程的方法
    * mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。
    * 当线程已经执行完成是 无论mayInterruptIfRunning 为true或false,结果都是false,即如果取消已经完成的任务会返回false;
    * 任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;
    * 如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回tr
    */
    boolean cancel(boolean mayInterruptIfRunning);
    /*
    * 用于检验线程是否被中断,如果是在完成前中断的就返回true
    * 在线程还没有开始的时候调用cancel 之后查询中断结果也是true
    */
    boolean isCancelled();
    /*
    * 判断任务是否已经完成,如果此任务完成,则返回true
    * 完成可能是由于正常终止、异常或取消——在所有这些情况下,此方法将返回true
    */
    boolean isDone();
    /*
    * 获取执行的结果,如果任务还未执行结束,那么就会一直等待,直到任务执行完毕
    * 
    */
    V get() throws InterruptedException, ExecutionException;
    /*
    * 有限等待,如果在特定时间内无法得到结果,那么就抛出TimeoutException不再进行等待
    * 
    */
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

        Future接口定义一个异步任务计算的方法,就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。对于get方法这里需要说一下,有限等待和无线等待的get方法,都执行中断响应,如果调用线程的interrupt方法都会抛出一个异常,然后不再等待,还有一点就是如果异步执行的线程在执行任务时,抛出了异常,那么调用get方法进行等待的线程会抛出一个ExecutionException的异常。

        接下来看一下RunnableFuture接口:

public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

         RunnableFuture接口就一个run方法,用于进行异步的执行任务,在使用之前先看一下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) {
	this.callable = Executors.callable(runnable, result);
	this.state = NEW;       // ensure visibility of callable
}

         FutureTask就两个构造器,一个是传递Callable,一个是需要传递Runnable和一个返回的参数,传递Runnable和一个返回的参数会使用适配器模式封装成Callable接口,因为FutureTask主要是拿到异步计算的结果,所以需要传递封装成Callable以拿到结果。

        这里大家可能会迷糊,为啥FutureTask类实现了Runnable接口,而不直接实现Callable接口,这是因为需要统一规范,在线程池框架中,提交的任务需要是Runnable接口的子类,所以FutureTask如果要在线程池中执行任务就要实现Runnable接口,而在FutureTask的run方法内部,最终会调用Callable接口的call方法。

         在进行源码分析之前,需要先说一下FutureTask的使用,这里就使用线程池的方式进行演示:

public class FutureTaskTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        //封装异步指定的任务,我们可以在主线程里面操作异步线程,如取消,查看是否已完成任务,或者
        //是得到结果
        Future<Integer> task = executorService.submit(() -> {
            //假装有业务代码正在执行
            System.out.println("异步任务开始执行了");
            TimeUnit.SECONDS.sleep(5);
            return 10;
        });
        System.out.println("我要获取任务了");
        try {
            Integer integer = task.get(3, TimeUnit.SECONDS);
            System.out.println("执行结果 "+integer);
        } catch (TimeoutException e) {
            System.out.println("这么长时间都等不到,溜了溜了");
            e.printStackTrace();
        }
    }
}

/*
it.cast.Thread.FutureTaskTest
我要获取任务了
异步任务开始执行了
这么长时间都等不到,溜了溜了
java.util.concurrent.TimeoutException
	at java.util.concurrent.FutureTask.get(FutureTask.java:205)
	at it.cast.Thread.FutureTaskTest.main(FutureTaskTest.java:16)
 */

         可以看到使用线程池执行异步方式进行操作时,使用的是submit方法,如果执行的是一般方法是使用execute方法,

                    

         如上图可以看到,使用submit方法是,需要传递一个Callable,然后线程池会根据Callable创建一个FutureTask类,然后使用execute方法提交任务,这里说一下,execute方法需要传递一个Runnable,这也说明了为啥FutureTask需要实现Runnable接口。

         接着说FutureTask类的使用,上面那个例子可能太简单,导致大家不能结合使用场景很好的体会FutureTask的强大,那么现在说一个我们公司使用场景:用户提交一个线上打包任务(就是写的代码打个一个安装包),用户提交打包任务之后,可以做其他事情,然后在打包完成后,给用户发送打包完成的邮件(该场景有多种解决方案,本例仅用来演示FutureTask类的使用)。

public class FutureTaskTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<String> task = executorService.submit(() -> {
            //假装有打包任务正在执行
            System.out.println("异步任务开始执行了");
            TimeUnit.SECONDS.sleep(5);
            return "打包完成";
        });
        //新启一个线程等待打包任务完成后获取到任务然后发送邮件
        executorService.execute(()->{
            String res = null;
            try {
                res = task.get();
            } catch (InterruptedException e) {
                res = "任务被中断";
                e.printStackTrace();
            } catch (ExecutionException e) {
                res = "任务执行过程中发生了错误";
                e.printStackTrace();
            }
            System.out.println(res);
            sendEmail(res);
        });

    }
}

        这里涉及到了三个线程,主线程即执行main方法的线程,异步执行打包任务的线程,异步等到打包任务完成的线程,主线程需要提交两个任务到线程池即可返回,在内部等异步执行打包任务完成后,会向用户发送邮件提醒。

       下面就开始讲解FutureTask的源码,FutureTask的源码非常简单,加上注释也就486行,现在就一起看一下。

public class FutureTask<V> implements RunnableFuture<V> {
    /**
     * The run state of this task, initially NEW.  The run state
     * transitions to a terminal state only in methods set,
     * setException, and cancel.  During completion, state may take on
     * transient values of COMPLETING (while outcome is being set) or
     * INTERRUPTING (only while interrupting the runner to satisfy a
     * cancel(true)). Transitions from these intermediate to final
     * states use cheaper ordered/lazy writes because values are unique
     * and cannot be further modified.
     *
     * 以下为state状态的切换, 
     * Possible state transitions:
     * NEW -> COMPLETING -> NORMAL
     * NEW -> COMPLETING -> EXCEPTIONAL
     * NEW -> CANCELLED
     * NEW -> INTERRUPTING -> INTERRUPTED
     */
    private volatile int state;
    private static final int NEW          = 0;//新建状态,当new一个FutureTask时,进行该状态
    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;//已经被中断

    /** 需要执行的任务 */
    private Callable<V> callable;
    /** 保存调用get方法的放回结果,如果state的状态为EXCEPTIONAL,那么结果就是一个异常信息 */
    private Object outcome; // non-volatile, protected by state reads/writes
    /** 执行任务的线程 */
    private volatile Thread runner;
    /** 调用get方法,等待执行结果的线程队列 */
    private volatile WaitNode waiters;
    
    // Unsafe mechanics
    //保存各属性中的相对于对象中的内存偏移量,使用Unsafe进行操作
    private static final sun.misc.Unsafe UNSAFE;
    private static final long stateOffset;
    private static final long runnerOffset;
    private static final long waitersOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> k = FutureTask.class;
            stateOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("state"));
            runnerOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("runner"));
            waitersOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("waiters"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

         FutureTask使用state标识位来判断当前所处的状态是成功、取消还是发生异常,该状态位非常重要,也是理解FutureTask的关键,其状态变化的过程是不可逆的,

                 

         另外还使用Unsafe类获取其属性的内存偏移量,方便进行CAS操作,一个java对象可以看成是一段内存,各个字段都得按照一定的顺序放在这段内存里,同时考虑到对齐要求,可能这些字段不是连续放置的,用这个UNSAFE.objectFieldOffset()方法能准确地告诉你某个字段相对于对象的起始内存地址的字节偏移量,因为是相对偏移量,所以它其实跟某个具体对象又没
什么太大关系,跟class的定义和虚拟机的内存模型的实现细节更相关。 

         首先来分析一个run方法,run方法代表启动任务,

public void run() {
	//1.判断状态是否为NEW,也就是新建状态,如果不是,则说明任务已经执行过,使用CAS将运行任务的线程
	//置位当前线程,失败的话直接判断
	if (state != NEW ||
		!UNSAFE.compareAndSwapObject(this, runnerOffset,null, Thread.currentThread()))
		return;
	try {
		//2. 得到需要执行的任务
		Callable<V> c = callable;
		//3.判断任务是否为空,再次判断状态是否为新建状态,
		if (c != null && state == NEW) {
			//保存执行结果
			V result;
			boolean ran;
			try {
				//4.调用任务,也就是Callable接口的,将结果传递给result
				result = c.call();
				ran = true;
			} catch (Throwable ex) {
				//5.走到次,说明在执行call方法的时候抛出了异常
				result = null;
				ran = false;
				//6.设置异常信息
				setException(ex);
			}
			if (ran)
				//7.如果能进入这个判断,则说明call方法中没有出现异常,设置结果
				set(result);
		}
	} finally {
		// runner must be non-null until state is settled to
		// prevent concurrent calls to run()
		//8.将执行任务的线程置位null
		runner = null;
		// state must be re-read after nulling runner to prevent
		// leaked interrupts
		int s = state;
		//9.如果线程处于正在中断,或是中断完成的状态,则会执行该方法
		if (s >= INTERRUPTING)
			handlePossibleCancellationInterrupt(s);
	}
}
//设置异常信息的方法
protected void setException(Throwable t) {
	//通过CAS将当前状态从NEW置位COMPLETING,快要完成的状态
	if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
		//将异常传给输出结果
		outcome = t;
		//此时再通过CAS将COMPLETING状态改变成EXCEPTIONAL异常状态
		UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
		//调用完成方法,用于唤醒调用get方法阻塞的线程
		finishCompletion();
	}
}
//设置异输出结果的方法,执行该方法说明,任务正常执行了,能够得到返回结果
protected void set(V v) {
	//通过CAS将当前状态从NEW置位COMPLETING,快要完成的状态
	if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
		//将任务执行结果赋值给outcome
		outcome = v;
		此时再通过CAS将COMPLETING状态改变成NORMAL正常状态
		UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
		//调用完成方法,用于唤醒调用get方法阻塞的线程
		finishCompletion();
	}
}

           首先根据state状态进行判断当前任务是否已经被其他线程执行过,如果没有则开始执行任务,任务执行分为下面两种情况:

           1.如果任务正常执行完成,拿到任务执行的结果,通过CAS将当前状态置位正在完成的状态,将结果设置之后,再通过CAS将当前状态置为完成状态。

           2.如果任务发生异常,拿到任务执行的结果,通过CAS将当前状态置位正在完成的状态,将异常信息设置之后,再通过CAS将当前状态置为异常状态。

           之后再通过finishCompletion方法唤醒等待的线程,关于finishCompletion等说完get方法之后,再介绍。

           get方法介绍,get方法可以获取到任务执行的结果,注意,需要明白一点,调用get方法进行等待结果的线程可能不只有一个。


public V get() throws InterruptedException, ExecutionException {
	int s = state;
	//1.判断当前状态是否处于未完成状态
	if (s <= COMPLETING)	
		//2.进入等待任务完成
		s = awaitDone(false, 0L);
	//3.返回结果
	return report(s);
}

public V get(long timeout, TimeUnit unit)
	throws InterruptedException, ExecutionException, TimeoutException {
	if (unit == null)
		throw new NullPointerException();
	//1.判断当前状态是否处于未完成状态
	int s = state;
	//2.进入有限等待,如果在等待规定时间内,任务还没有完成,则抛出一个异常,不再进行等待
	if (s <= COMPLETING &&
		(s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
		throw new TimeoutException();
	//3.返回结果
	return report(s);
}

private int awaitDone(boolean timed, long nanos)throws InterruptedException {
	//1.判断是有限等待还是无限等待
	final long deadline = timed ? System.nanoTime() + nanos : 0L;
	WaitNode q = null;
	boolean queued = false;
	//2.执行死循环,根据状态和等待时间执行相应的逻辑
	for (;;) {
		//3.如果线程被中断,移除等待的线程,并抛出异常
		if (Thread.interrupted()) {
			removeWaiter(q);
			throw new InterruptedException();
		}

		int s = state;
		//4.如果状态大于正在完成的状态,说明已经完成了,此时直接返回结果
		if (s > COMPLETING) {
			if (q != null)
				q.thread = null;
			return s;
		}
		//5.如果是正在完成的状态,就让当前线程让出cpu,从运行状态改为就绪状态,
		//因为任务就要完成了,此时让线程睡眠的话,就不值得了,因为正要完成的状态和
		//已完成的状态之间的切换时很快的
		else if (s == COMPLETING) // cannot time out yet
			Thread.yield();
		//6.如果q == null,第6步和第7步是调用get方法时,一般会执行的,因为在
		//死循环之前设置了WaitNode q = null;,所以会先进入第6步,以当前线程为节点创建
		//一个WaitNode节点,然后第二次循环时,进行第7步,加入到等待队列中
		else if (q == null)
			q = new WaitNode();
		//7.加入到等待队列中	
		else if (!queued)
			queued = UNSAFE.compareAndSwapObject(this, waitersOffset,q.next = waiters, q);
		//8.第8步和第9步是根据有限等待和无限等待,然后调用LockSupport的阻塞方法阻塞当前线程
		else if (timed) {
			nanos = deadline - System.nanoTime();
			if (nanos <= 0L) {
				removeWaiter(q);
				return state;
			}
			LockSupport.parkNanos(this, nanos);
		}
		//9.阻塞
		else
			LockSupport.park(this);
	}
}

         调用get方法会阻塞当前线程,根据当前任务执行的状态 和等待的时间进行判断,需要注意的是第6步和第7步,这两步会先添加一个等待节点然后添加到等待队列中,然后第8步,第9步根据是有限等待还是无限等待,调用LockSupport进行阻塞当前线程。

        在等待之后会调用report方法,根据state状态位判断是返回执行结果还是抛出一个异常。

private V report(int s) throws ExecutionException {
	//1.outcome就是任务执行的结果
	Object x = outcome;
	//2.NORMAL代表正常结束,有返回值
	if (s == NORMAL)
		return (V)x;
	//3.任务被取消
	if (s >= CANCELLED)
		throw new CancellationException();
	//4.走到这一步,说明是在执行任务时发生的异常,此时将异常抛给调用get方法的线程
	throw new ExecutionException((Throwable)x);
}

         到此,FutureTask源码就分析完了,FutureTask充分体现了Java并发工具类的三板斧:状态,队列,CAS,这一点和AQS有异曲同工之妙,首先使用state状态位来标识当前任务执行的状态,使用队列来保存调用get方法进行阻塞的线程,使用CAS完成其状态的修改和入队出队的操作。

        总结:

        FutureTask能够进行异步计算,实现了Runnable和Future接口,实现Runnable的run方法用于执行任务,实现Future接口的方法用于异步执行任务时,可以很方法的掌控任务的执行,如取消任务,判断任务是否执行完毕,获取任务执行的结果,其中在FutureTask中比较重要的就是任务当前的状态了,因为在FutureTask中几乎所有的操作都需要使用到state状态位,这个state状态位使用volatile进行修饰保证可见行,使用队列来保存调用了get方法进行阻塞的线程,记住调用get方法获取结果的线程可能不只有一个,所以其入队操作使用到了CAS,然后调用LockSupport进行等待,等待任务执行完毕后,唤醒等待队列中的所有线程。

         对于实现Runnable的run方法用于执行任务,如果任务正常执行结束,会得到任务执行的结果,然后去唤醒等待队列中等待的线程,如果发生异常,会得到异常信息,然后去唤醒等待队列中等待的线程。

         对于实现Future接口的get方法,会将该线程封装到Node节点,然后通过CAS的方法加入到等待队列中,然后根据是有限等待或是无限等待,调用LockSupport的park方法进行阻塞当前线程,当任务执行完毕之后,会唤醒等待的线程,此时调用get方法阻塞的线程会被唤醒,然后获取任务执行的结果,其结果是根据state状态进行判断的,如果是正常结果,那么状态就是NORMAL,此时会返回任务执行的结果,如果任务抛出了异常,那么就将这个异常抛给调用了get方法的线程。

参考文章:

FutureTask源码解析(2)——深入理解FutureTask

Java并发系列-深入Jvm理解Thread启动流程

FutureTask详解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值