0.前言
在Java中一般通过继承Thread类或者实现Runnable接口这两种方式来创建多线程,但是这两种方式都有个缺陷,就是不能在执行完成后获取执行的结果,因此Java 1.5之后提供了Callable和Future接口,通过它们就可以在任务执行完毕之后得到任务的执行结果。 在一些开源框架中经常都可以看到FutureTask的身影, 本文会简要的介绍FutureTask使用方法,然后会从源代码角度分析下具体的实现原理。
1.FutureTask使用
这里使用了一个最简单的demo,来介绍FutureTask的应用与一些主要方法接口。
// 1.新建FutureTask
FutureTask<Integer> futureTask = new FutureTask<>(() -> {
System.out.println("正在计算结果...");
Thread.sleep(3000);
return 1;
});
// 2.新建Thread对象并启动
Thread thread = new Thread(futureTask);
thread.start();
// 3.新建其他Thread对象并启动,执行等待任务
Thread thread1 = new Thread(() -> {
try {
//进入等待队列,不同的线程
futureTask.get();
} catch (Exception e) {
e.printStackTrace();
}});
thread1.start();
// 4.主线程休眠
Thread.sleep(1000);
// 5.主线程调用isDone()判断任务是否结束
if(!futureTask.isDone()) {
System.out.println("Task is not done");
}
// 6.调用get()方法获取任务结果,如果任务没有执行完成则阻塞等待
System.out.println("result is " + futureTask.get());
// 7.主线程休眠Thread.sleep(2000);
if(futureTask.isDone()) {
System.out.println("Task is done");
}
// 8.调用get()方法获取任务结果,如果任务没有执行完成则阻塞等待
System.out.println("result is " + futureTask.get());
常用的接口说明
简单说明一下接口定义
boolean cancel(boolean mayInterruptInRunning) 取消一个任务,并返回取消结果。参数表示是否中断线程。
boolean isCancelled() 判断任务是否被取消Boolean isDone() 判断当前任务是否执行完毕,包括正常执行完毕、执行异常或者任务取消。
V get() 获取任务执行结果,任务结束之前会阻塞。
V get(long timeout, TimeUnit unit) 在指定时间内尝试获取执行结果。若超时则抛出超时异常
2.FutureTask原理
2.1 变量定义
Callable callable:被提交的任务Object
outcome:任务执行结果或者任务异常
volatile Thread runner:执行任务的线程
volatile WaitNode waiters:等待节点,关联等待线程
long stateOffset: state字段的内存偏移量
long runnerOffset:runner字段的内存偏移量
long waitersOffset:waiters字段的内存偏移量
2.2 内部状态转移
FutureTask内部需要处理任务执行的多钟状态,如任务正常完成/任务发生异常/取消任务/任务中断等多种状态,在源码中FutureTask使用了状态位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;
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;
其中需要注意的是state是volatile类型的,也就是说只要有任何一个线程修改了这个变量,那么 其他所有的线程都会知道最新的值。为了后面更好的分析FutureTask的实现,这里有必要解释下各个状态。
- NEW:表示是个新的任务或者还没被执行完的任务。这是初始状态。
- COMPLETING:任务已经执行完成或者执行任务的时候发生异常,但是任务执行结果或者异常原因还没有保存到outcome字段(outcome字段用来保存任务执行结果,如果发生异常,则用来保存异常原因)的时候,状态会从NEW变更到COMPLETING。但是这个状态会时间会比较短,属于中间状态。
- NORMAL:任务已经执行完成并且任务执行结果已经保存到outcome字段,状态会从COMPLETING转换到NORMAL。这是一个最终态。
- EXCEPTIONAL:任务执行发生异常并且异常原因已经保存到outcome字段中后,状态会从COMPLETING转换到EXCEPTIONAL。这是一个最终态。
- CANCELLED:任务还没开始执行或者已经开始执行但是还没有执行完成的时候,用户调用了cancel(false)方法取消任务且不中断任务执行线程,这个时候状态会从NEW转化为
- CANCELLED状态。这是一个最终态。
- INTERRUPTING: 任务还没开始执行或者已经执行但是还没有执行完成的时候,用户调用了cancel(true)方法取消任务并且要中断任务执行线程但是还没有中断任务执行线程之前,状态会从NEW转化为INTERRUPTING。这是一个中间状态。
- INTERRUPTED:调用interrupt()中断任务执行线程之后状态会从INTERRUPTING转换到INTERRUPTED。这是一个最终态。
有一点需要注意的是,所有值大于COMPLETING的状态都表示任务已经执行完成(任务正常执行完成,任务执行异常或者任务被取消)。
2.2 主要流程
通过上面的demo可以看到主线程在新建完FutureTask后一直阻塞等待结果,任务线程负责执行任务,并将最后结果塞回到输出outcome字段,并通知等待线程,由于可以由多个线程阻塞等待,在内部实现中使用等待链表和LockSupport的park/unpark操作来进行不同线程之间的同步操作,大致流程如下所示。
1.在主线程/其他等待线程中可以调用awaitDone,该逻辑会自旋并调用park进行阻塞等待,如果任务还未完成则放入等待链中;
2.执行线程执行完任务后遍历等待链表,对每个节点进行unpark操作;
3.在awaitDone()逻辑中会收到unpark同步信号,唤醒对应的线程已执行完任务。
2.3 核心方法解析
2.3.1 public void run()
当任务被执行时,会经过一下几个步骤:
1.再次检查任务状态state;
2.执行任务逻辑,调用c.call()执行;
3.如果业务逻辑正常将会调用set方法将执行结果赋予到outcome,并且更新state值;
4.如果业务逻辑异常将会调用setException方法将异常对象赋予到outcome,也更新state值.
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);
}
} finally {
runner = null;
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
protected void set(V v) {
//state状态从New->Completing
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
//state状态从Completing->Normal UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}
protected void setException(Throwable t) {
//state状态从New->Completing
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = t;
//state状态从Competing->Exptional
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
finishCompletion();
}
}
2.3.2 get()和get(long timeout, TimeUnit unit)
任务由其他线程执行,主线程则会阻塞,直到任务线程唤醒它们。我们通过get(long timeout, TimeUnit unit)方法看看是怎么实现的。
public V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
if (unit == null)
throw new NullPointerException();
int s = state;
if (s <= COMPLETING &&
(s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
throw new TimeoutException();
return report(s);
}
/**
* 等待任务执行完毕,如果任务取消或者超时则停止 * @param timed 为true表示设置超时时间 * @param nanos 超时时间 */
private int awaitDone(boolean timed, long nanos)
throws InterruptedException {
// 任务截止时间
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)
// 可能任务线程被阻塞了,主线程让出CPU
Thread.yield();
else if (q == null)
// 等待线程节点为空,则初始化新节点并关联当前线程
q = new WaitNode();
else if (!queued)
// 等待线程入队列,成功则queued=true
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
// timed=false时会走到这里,挂起当前线程
LockSupport.park(this);
}
}
2.3.3 finishCompletion()
当任务线程执行完后,会在finishCompletion()方法中进行唤醒操作,其中WaitNode节点中存储了等待线程的信息,这里做了链表的遍历操作,对每一个等待线程进行LockSupport.unprk操作,唤醒等待的线程。
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;
// unlink to help gc
q.next = null;
q = next;
}
break;
}
}
//模板方法,可以被覆盖
done();
//清空callable
callable = null;
}
2.3.4 public boolean cancel(boolean mayInterruptIfRunning)
1. state不为NEW时,任务即将进入终态,直接返回false表明取消操作失败。
2. state状态为NEW,任务可能已经开始执行,也可能还未开始。
3. mayInterruptIfRunning表明是否中断线程。若是,则尝试将state设置为INTERRUPTING,并且中断线程,之后将state设置为终态INTERRUPTED。
4. 如果mayInterruptIfRunning=false,则不中断线程,把state设置为CANCELLED
5. 移除等待线程并唤醒。
6. 返回true
public boolean cancel(boolean mayInterruptIfRunning) {
if (state != NEW)
return false;
if (mayInterruptIfRunning) {
if (!UNSAFE.compareAndSwapInt(this, stateOffset, NEW, INTERRUPTING))
return false;
Thread t = runner;
if (t != null)
t.interrupt();
UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED); // final state
}
else if (!UNSAFE.compareAndSwapInt(this, stateOffset, NEW, CANCELLED))
return false;
finishCompletion();
return true;
}
2.3.5 LockSupport
JDK中提供了很多-多线程的工具类,其中LockSupport就是一个很好的实现,通过功能接口可以很容易实现按照要求调度线程,如暂停/恢复线程。
public static void park(Object blocker); // 暂停当前线程
public static void parkNanos(Object blocker, long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(Object blocker, long deadline); // 暂停当前线程,直到某个时间
public static void park(); // 无期限暂停当前线程
public static void parkNanos(long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(long deadline); // 暂停当前线程,直到某个时间
public static void unpark(Thread thread); // 恢复当前线程
public static Object getBlocker(Thread t);
Thread t = new Thread(() -> {
System.out.println("park before");
LockSupport.park();
if
System.out.println("park after");
});
t.start();
Thread.sleep(3000L);
LockSupport.unpark(t);
这里的park/unpark可以实现类似于wait/notify的功能, park/unpark使用的是unsafe类来实现的,其中具体的实现逻辑需要翻看JDK源码。
public static void park() {
UNSAFE.park(false, 0L);
}
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
//c++代码实现
class Parker : public os::PlatformParker {
private:
volatile int _counter ;
...
public:
void park(bool isAbsolute, jlong time);
void unpark();
...
}
class PlatformParker : public CHeapObj<mtInternal> {
protected:
pthread_mutex_t _mutex [1] ;
pthread_cond_t _cond [1] ;
...
}
3.总结
本篇通过对FutureTask的简单使用与原理实现的深入分析,对FutureTask的线程模型与线程间的同步方式有了了解 ,并对LockSupport与Unsafe工具类有了简单的认识。这种异步处理任务的思路已广泛应用在多线程与网络编程中, 能熟练掌握对多线程编程的水平提高很有好处。