前言
本来打算下班回来就开写的,打开电脑,面对无尽的源码,审视自己疲惫的躯壳,发现无论如何下不了手。于是翻开了《闯入不适区》,刺激一下精神,接着去洗了个澡。回来已是23:40,我咬了咬牙,还是输出一点吧,能写多少是多少。
介绍cancel功能
FutureTask实现了cancel功能,该功能就是等待线程可以对正在执行或即将执行的任务发送cancel命令,本质上就是对执行该任务的线程发送一个interrupt标识。此处翻译一下源码的方法。
/**
* Attempts to cancel execution of this task. This attempt will
* fail if the task has already completed, has already been cancelled,
* or could not be cancelled for some other reason. If successful,
* and this task has not started when {@code cancel} is called,
* this task should never run. If the task has already started,
* then the {@code mayInterruptIfRunning} parameter determines
* whether the thread executing this task should be interrupted in
* an attempt to stop the task.
*
* <p>After this method returns, subsequent calls to {@link #isDone} will
* always return {@code true}. Subsequent calls to {@link #isCancelled}
* will always return {@code true} if this method returned {@code true}.
*
* @param mayInterruptIfRunning {@code true} if the thread executing this
* task should be interrupted; otherwise, in-progress tasks are allowed
* to complete
* @return {@code false} if the task could not be cancelled,
* typically because it has already completed normally;
* {@code true} otherwise
*/
boolean cancel(boolean mayInterruptIfRunning);
这里分享一个阅读源码的心得:看接口注释,包括接口、方法注释。对于接口方法来说,首先,它通常对其实现功能、注意事项、用法、参数和返回值含义等进行了定义,可以说是一种约定。尽管其实现类有各种不同的实现,但必须遵守其接口的约定。只有首先了解约定,才能更好的去理解每种不同的实现,做到纲举目张。一个约定,有各种千奇百怪的实现,这也是一件十分有趣、十分令人好奇的事情。
话不多说,对上面的英文进行一波翻译(上一次翻译不是为了毕业就是在考研):
尝试取消该任务的执行。如果这个任务已经执行完成,或者已经被取消,又或者由于一些其他原因不能被取消,本次取消的动作将失败(不会带来任何变化或影响)。如果取消成功,并且该任务在该方法被调用之前还没有开始,这个任务将永不会被执行。如果一个任务已经开始,那么根据mayInterruptIfRunning这个参数来决定是否要中断(interrupt)一个正在执行任务的线程来停止任务。
在这个方法返回之后,后续对isDone()方法的调用都将会true。如果该方法返回true,那么后续对isCancelled方法的调用也都将返回true。
@param mayInterruptIfRunnning为true,代表正在执行该任务的线程将被中断(interrupt);否则,正在执行的任务就继续执行直到完成。
@return为false,代表该任务无法被取消,通常这是因为任务已经正常执行完毕;其他情况则返回true。
0913 continue
阅读cancel源码
解析任务状态
增加cancel功能之后,原任务状态只有完成/未完成两个状态,使用的是boolean类型,当前需要新增一个取消状态,因此需要修改该字段改为int state。同时,我们需要列举所有可能的状态:
- NEW:代表初始状态
- FINISHIED:代表任务已完成
- CANCELED:代表人物已取消
- INTERRUPTED:已中断。该字段与
cancel方法的入参对应,当任务正在执行时,接收到取消的命令,则在适当时候停下任务,进入中断状态。
Class MyFutureTask {
private volatile int state;
private static final int NEW = 0;
private static final int FINISHED = 1;
private static final int CANCELLED = 2;
private static final int INTERRUPTED = 3;
}
而实际源码还添加了COMPELTING和INTERRUPTING两个状态。
阅读cancel方法
下面我们先来解析cancel方法。根据接口描述,cancel失败的情况有三种:
- 任务已完成;
- 任务已被取消(不能重复取消);
- 由于其他原因无法被取消(主要是多线程并发取消时,只能有一个取消成功);
cancel成功的情况有两种:
mayInterruptIfRunning为true,若线程正在执行,将发送中断信号,最终state为INTERRUPTED;- 否则,若线程正在执行,将允许执行完毕,最终state为
CANCELLED。
一旦任务被成功取消,等待队列中的线程都将会唤醒,且调用get()最终会抛出异常CancellationException。
public boolean cancel(boolean mayInterruptIfRunning) {
if (!(state == NEW &&
UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
return false;
try { // in case call to interrupt throws exception
if (mayInterruptIfRunning) {
try {
Thread t = runner; // 这里再一次用到了快照模式
if (t != null)
t.interrupt(); // 此处可能runner已经为null,代表任务已经执行完毕
} finally { // final state
UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
}
}
} finally {
finishCompletion();// 唤醒所有的等待线程
}
return true;
}
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable<V> 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); // 如果此时已经被cancel,则该方法无影响
}
if (ran)
set(result); // 同上
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
根据源码说明几种并发状态
- 中断正在执行的线程,线程响应了中断并清除;
| 执行线程 | 等待线程 |
|---|---|
| 起始是NEW状态 | |
| 1 c.call() // 任务执行中 | |
| 2. NEW -〉 INTERRUPTTING | |
| 3. t.interrupt() // 中断执行线程 | |
| 4. setException(ex) //执行到可中断点抛出中断异常 | |
| 6. INTERRUPTTING -> INTERRUPTED | |
| 7. 执行finaly | |
| 8. 执行finally |
- 中断正在执行的线程,但执行线程没有中断点,导致执行线程在执行期间内未响应,最终也未清除中断标记。
| 执行线程 | 等待线程 |
|---|---|
| 起始是NEW状态 | |
| 1 c.call() // 任务执行中 | |
| 2. NEW -〉 INTERRUPTTING | |
| 3. t.interrupt() // 中断执行线程 | |
| 4. set(result) // 无可中断点 | |
| 6. INTERRUPTTING -> INTERRUPTED | |
| 7. handlePossibleCancellationInterrupt | |
| 8. 执行finally |
根据handlePossibleCancellationInterrupt的源码可以看到,作者将清除中断标记的代码注释掉了,这是为了让该中断保留在执行线程中,让后续的代码得知该线程被中断过,也就是该中断标记虽然在线程执行当前任务的过程中没有被响应,但后续到达可中断点时依然会被响应。
private void handlePossibleCancellationInterrupt(int s) {
// It is possible for our interrupter to stall before getting a
// chance to interrupt us. Let's spin-wait patiently.
if (s == INTERRUPTING)
while (state == INTERRUPTING)
Thread.yield(); // wait out pending interrupt
// assert state == INTERRUPTED;
// We want to clear any interrupt we may have received from
// cancel(true). However, it is permissible to use interrupts
// as an independent mechanism for a task to communicate with
// its caller, and there is no way to clear only the
// cancellation interrupt.
//
// Thread.interrupted(); // 这里被注释掉了。。。
}
- 想要中断正在执行的线程,但执行线程已经与任务脱钩了,导致没有发送中断信号。
| 执行线程 | 等待线程 |
|---|---|
| 起始是NEW状态 | |
| 1 c.call() // 任务执行中 | |
| 2. NEW -〉 INTERRUPTTING | |
| 3. set(result) // 无效果 | |
| 4. runner = null | |
| 5. handlePossibleCancellationInterrupt | |
| 6. t = runner | |
| 7. if(t!=null) // 为空,不会interrupt了 | |
| 8. INTERRUPTTING -> INTERRUPTED | |
| 9. finally |
这种情况下,等待线程以为自己发送成功了中断信号,但实际上因为线程调度的问题,执行线程并没有收到中断信号,既没有清除,也没有传递可言。
那么问题就是,第2点和第3点,线程调度的时机不同,代码的执行结果不一致,是否有问题??
结语
FutureTask的基本原理介绍就差不多了,核心的地方以及大体实现思路均已说明,其他的边边角角自行看代码即可掌握。本次最大的感受是,阅读源码不容易,读懂后输出更难,通俗易懂的输出难上加难!