文章目录
1.Callable概述
1.1.Callable与Runable的区别
Callable
是一个用于定义多线程执行的代码逻辑的接口,里面定义了一个call()
方法,类似于Runnable
中的run()
方法。子类实现了Callable
接口后就可以重写call()
方法的执行逻辑,然后交给系统调度就好了。那么两者的区别是什么呢?
Callable
除了执行多线程的代码逻辑之外,还支持返回值和异常抛出,同时还支持主线程阻塞等待子线程返回数据,这也是与Runnable
的主要区别。
1.2.Callable的使用
Callable
这个接口并没有什么特殊的,只是定义了一个call()
,它的特性是依赖于FutureTask
类来实现的,FutureTask
的顶部接口实现了Runnable
和Future
接口,类图如下。
下面使用一个简单的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.阻塞的实现
父线程想要获取子线程的返回值,前提条件是子线程一定执行完毕了,如果在子线程执行完毕之前,父线程调用FutureTask
的get()
方法就会先进入阻塞,阻塞的实现和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;
}