【并发编程】(十九)Callable的实现原理简述

1.Callable概述

1.1.Callable与Runable的区别

Callable是一个用于定义多线程执行的代码逻辑的接口,里面定义了一个call()方法,类似于Runnable中的run()方法。子类实现了Callable接口后就可以重写call()方法的执行逻辑,然后交给系统调度就好了。那么两者的区别是什么呢?

Callable除了执行多线程的代码逻辑之外,还支持返回值异常抛出,同时还支持主线程阻塞等待子线程返回数据,这也是与Runnable的主要区别。

1.2.Callable的使用

Callable这个接口并没有什么特殊的,只是定义了一个call(),它的特性是依赖于FutureTask类来实现的,FutureTask的顶部接口实现了RunnableFuture接口,类图如下。
在这里插入图片描述
下面使用一个简单的Demo来描述一下Callable的特性:阻塞和返回值

public class CallableDemo {

    public static String get1() {
        return "精忠";
    }

    public static String get2() {
        return "报国";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 使用匿名内部类实现Callable接口,重写call()方法
        Callable<String> call1 = new Callable() {
            @Override
            public String call() {
                return get1();
            }
        };
        Callable<String> call2 = new Callable() {
            @Override
            public String call() {
                return get2();
            }
        };

        // 创建FutureTask对象,将Callable包装起来
        FutureTask<String> futureTask1 = new FutureTask<>(call1);
        FutureTask<String> futureTask2 = new FutureTask<>(call2);
        // 启动线程
        Thread t1 = new Thread(futureTask1);
        Thread t2 = new Thread(futureTask2);
        t1.start();
        t2.start();

        // 当前线程阻塞,等待t1执行完毕
        String str1 = futureTask1.get();
        // 当前线程阻塞,等待t2执行完毕
        String str2 = futureTask2.get();

        // 打印执行结果
        System.out.println(str1 + str2);
    }
}

通过get()方法就可以阻塞当前线程,等待子线程执行完毕后,获取返回值,最终打印出:

精忠报国

2.实现原理简述

2.1.Callable是如何被线程调度的

首先,线程的启动一定是通过Thread对象的start()方法,在Thread类的构造方法中,可以传入一个Runnable作为参数,线程启动后,操作系统调度线程回调Runnable中的run()方法,达到异步调用的效果。
FutureTask类的顶层接口中实现了Runnable接口,线程获取到CPU资源后,就会去回调FutureTask中的run()方法,所以只需要在run()方法中去调用call()就实现了Callable的调度。
下面的源码省略了部分代码,只保留了最核心的部分。

public class FutureTask<V> implements RunnableFuture<V> {

	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;

	public void run() {
	    Callable<V> c = callable;
	    if (c != null && state == NEW) {
	        V result;
	        boolean ran;
	        try {
	        	// 发起Callable的调度,并获取返回值
	            result = c.call();
	            ran = true;
	        } catch (Throwable ex) {
	            result = null;
	            ran = false;
	            // 如果报错,就抛出异常
	            setException(ex);
	        }
	        if (ran)
	        	// 设置返回值
	            set(result);
	    }
	}
}

核心代码还是比较好理解的,调用成功就设置返回值,调用失败就设置异常,两个方法都是赋值给一个成员变量outcome

public class FutureTask<V> implements RunnableFuture<V> {

	private Object outcome;
	
	protected void set(V v) {
		// 将当前的FutureTask替换为完成状态
	    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
	    	// 保存返回值
	        outcome = v;
	        // 保存完成后替换为正常状态
	        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
	        finishCompletion();
	    }
	}
	
	protected void setException(Throwable t) {
	    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
	        outcome = t;
	        // 保存成功后替换成异常状态
	        UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
	        finishCompletion();
	    }
	}
}

2.2.返回值的获取

从上面的代码中可以看到,无论是正常的返回值还是异常的返回值都使用了同一个Object对象来接收,那么外层的父线程是如何接收到返回值的呢?
FutureTask还实现了一个Future接口,里面有一个get()方法可以用来获取子线程保存的返回值,get()的实现方法如下:

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    // 小于完成状态表示子线程还在执行中,当前线程需要等待(阻塞的实现)
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    // 如果返回值正常就正常返回,如果是异常就抛出
    return report(s);
}

上面的report(s)方法就是用来做返回值处理的,在返回结果时,需要等待子线程执行完毕并将返回值赋值给FutureTask的成员变量outcome ,下面先看看返回值是如何返回的,再看阻塞方法。

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);
}

上面就是返回结果的方法非常直观,就不多做描述了,接下来看一下阻塞的实现。

2.3.阻塞的实现

父线程想要获取子线程的返回值,前提条件是子线程一定执行完毕了,如果在子线程执行完毕之前,父线程调用FutureTaskget()方法就会先进入阻塞,阻塞的实现和AQS类似,就是将父线程包装在一个Node节点中,然后将它挂起。

private int awaitDone(boolean timed, long nanos) throws InterruptedException {

	// 等待队列
    private volatile WaitNode waiters;

    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    WaitNode q = null;
    boolean queued = false;
    // 自旋锁,这个锁里面有两个退出条件
    for (;;) {
    	// 线程被中断,不再等待
        if (Thread.interrupted()) {
            removeWaiter(q);
            throw new InterruptedException();
        }

		// 判断一下子线程现在是否执行完毕,如果已经执行完了就不需要阻塞了
		// 退出条件1
        int s = state;
        if (s > COMPLETING) {
            if (q != null)
                q.thread = null;
            return s;
        }
        // COMPLETING表示子线程已经执行完毕,正在给成员变量赋值,这个时候父线程让出CPU资源
        else if (s == COMPLETING) // cannot time out yet
            Thread.yield();
        else if (q == null)
            q = new WaitNode();   
        else if (!queued)
        	// 将新节点在等待队列中做头插入
            queued = UNSAFE.compareAndSwapObject(this, waitersOffset, q.next = waiters, q);
        // 如果设置了等待超时时间,将当前线程按超时时间挂起
        else if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) {
                removeWaiter(q);
                // 自旋锁退出条件2
                return state;
            }
            LockSupport.parkNanos(this, nanos);
        }
        // 没有配置超时时间就将当前现在直接挂起
        else
            LockSupport.park(this);
    }
}

上面代码中的等待队列的头插入,如果不熟悉的话会难理解一点,下面画个图来演示下头插入流程。
首先,对象初始化时,waiters指向null
在这里插入图片描述
现在一个线程调用了get()方法,且子线程还未执行完毕,此时就会将当前的线程加入到等待队列中,做头插入q.next=waiter,此时waiters=null,所以就有下面的结果。
在这里插入图片描述
然后是CAS操作,将waiters的原始值作为期望值,将q作为更新值,最终队列就变成了下面的样子。
在这里插入图片描述
FutureTask的常用方法中,父线程在这一步之后就会将自己挂起了,不会有新的线程再进入等待队列。但是如果有新的线程进入的话,再做一次头插入就可以了,假如有一个新的节点q1加入了等待队列,图示如下:
在这里插入图片描述

2.4.阻塞线程的唤醒

有阻塞必然有唤醒,没有等待时间的被挂起的线程是在哪里被唤醒的呢?
我们想一下,父线程的阻塞原因是子线程还没有执行完毕,所以在子线程执行完毕后就应该去把挂起的父线程唤醒。那唤醒的方法一定在run()方法中,在上面的两个保存返回值的set()方法中执行了finishCompletion(),父线程的唤醒就在这里面。

    private void finishCompletion() {
    	// waiters如果等于null,就没有线程处于等待状态
        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;
                    q = next;
                }
                break;
            }
        }
        done();
        callable = null;      
    }

3.Callble执行流程图

在这里插入图片描述

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

挥之以墨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值