概述
引入
我们之前介绍了Callable
和Runnable
接口,Callable
接口在使用时可能会遇到只支持Runnable
接口的多线程框架,那时我们使用了FutureTask
进行转换,我们可以根据用法大概猜一下FutureTask
的功能:
- 实现
Runnable
接口,从而可以适配只支持Runnble
入参的框架 - 支持
Callable
入参从而可以接受我们实现的Callable
类 - 支持对运行结果的检测,从而能保证我们在合适的时间获得我们
Callable
类运算的结果
我们本次介绍的接口Future
就提供了任务执行状态检测的功能。
摘要
本文从以下方面介绍Future
:
- 源码解析
- 使用方法
- 使用场景
类介绍
类定位
Future
接口定义了一些对子线程执行的任务进行执行状态检测及操作的功能,如果我们:
- 定义的子任务要进行状态检测、提前取消等操作
- 定义的子任务有数据要传出
满足上面任何一点,就有了依赖此接口的必要。当然,这种有点带着多线程同步功能的复杂函数我们是不需要专门去实现它的,太复杂了,一般我们还是:
- 实现
Callable
接口,将我们的逻辑包装成a
, - 依赖
Future
的某个实现类将a
包装成b
- 将
b
传入到某个线程中执行
注意
注意用法即可。
源码解读
取消接口
/**
* 尝试取消此任务的执行,如果下面有一种情况存在,则取消失败:
* 1. 任务执行完成
* 2. 任务已经失败
* 3. 任务无法取消
*
* 如果成功取消任务,则返回 true , 如果取消失败,则返回 false
*
* 取消机制:
* 1. 如果任务还没开始运行,就直接取消了,这个任务也不会再运行
* 2. 如果任务已经在运行了,就根据第二个入参 mayInterruptIfRunning ,决定是否调用 interrupt()
* 打断线程【注意:为什么是打断线程?打断线程有没有作用?这个参见扩展】
*
* 如果调用此方法返回了 true , 则此任务状态为: 已结束/已取消 【已取消也是结束状态的一种】
*
* @param mayInterruptIfRunning
* true : 表示如果该任务正在执行,就打断线程使其作出反应
* false : 表示如果该任务正在执行,就放任其执行
* @return 是否取消成功
*/
boolean cancel(boolean mayInterruptIfRunning);
检测是否已取消
/**
*
* @return true 如果任务已取消
* 注意:是取消状态不是完成状态。结束状态包括完成状态【正常执行完成】和取消状态【被取消】
*/
boolean isCancelled();
检测是否结束
/**
* 到达结束状态的几种可能路径:
* 1. 正常运行完成【可能执行完成,也可能是抛出异常导致直接进入完成状态】
* 2. 被提前取消
*
* @return true 如果任务是结束状态
*/
boolean isDone();
取运行结果
两种方法,一种是一直等,等到结束状态,一种是限时等待。
/**
* 等待任务完成,然后返回结果
*
* @return 结果
* @throws CancellationException 如果结束状态是其中的取消状态
* @throws ExecutionException 逻辑代码执行中抛出异常,就用 ExecutionException 包一下抛出来【见扩展】
* @throws InterruptedException 如果在等待任务完成的时候被打断了
* while waiting
*/
V get() throws InterruptedException, ExecutionException;
/**
* 除了限时,其他的没变化
*
* @param timeout 等待时间【数值】
* @param unit 等待时间【单位】
* @return 结果
* @throws CancellationException
* @throws ExecutionException
* @throws InterruptedException
* @throws TimeoutException 如果等待时间到达还没到达结束状态
*/
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
使用示例
示例
同上次记录的FutureTask
使用。
使用思路
- 使用
FutureTask
的get()
使主线程完成对子线程的等待。类似于join()
+两个线程共有同步变量控制 - 可以使用限时等待,提前取消等。
注意
我们取消的机制是线程中断,如果你在写逻辑时设定成对中断不反应切偷偷的吞了中断信号,那么取消时及时传入true
完成打断,表面上是取消了,但是子线程还是会占用线程资源,直到正常结束。【为什么这么做?有什么意义?可以参见扩展】
实现原理解析
我们在介绍具体实现类时就事论事吧。
问题
- 为什么我们取消时使用中断而非直接杀死线程?【参见扩展】
- Java 中断是如何工作?【参见扩展】
扩展
Java 中一些底层机制转换的原因【可能只是其中一点】
此部分完全来自参考文献的链接,本人只是对其中的知识点进行了一些总结。
Thread.stop()
用途
用来杀死线程。安静的杀死线程。
缺点
Thread.stop()
带来的一个很严重的问题是:它是不安全的。原因如下:
-
Thread.stop()
杀死线程的操作很安静,不会通知其他线程,很容易造成隐患。 - 使用
Thread.stop()
会释放此线程占有的所有锁,失去此线程的锁维护的对象的一致性可能会被破坏,会为接下来的执行造成很多不确定性。 - 如果通过捕捉ThreadDeath产生的
Exception
,然并进行线程停止后的扫尾工作,这样在用户编写多线程代码时会大大增加编码难度:- 用户要在几乎所有地方考虑ThreadDeath产生的
Exception
- 线程可能会多次抛出此异常,用户需要多次清理以完成目标,这会使用户代码大大复杂化
- 用户要在几乎所有地方考虑ThreadDeath产生的
转变
思路
- 首先,使用一个
volatile
变量来记录看是否应该结束线程- 在写代码时轮询这个变量,发现变成结束标志就停止线程推出【用户自己结束操作退出,很安全】
- 在调用
stop()
时,设置对应变量为结束标志,并调用线程中断
解决问题
- 通过中断标志位,可以传递信息
- 通过用户自行相应中断结束,更好的维护变量
- 降低了编码的复杂度
示例代码
private volatile Thread blinker;
public void stop() {
blinker = null;
}
public void run() {
Thread thisThread = Thread.currentThread();
while (blinker == thisThread) {
try {
thisThread.sleep(interval);
} catch (InterruptedException e){
}
repaint();
}
}
注意事项
- 我们使用了线程中断机制来避免过长的等待,使我们在设置标志量后能及时提醒线程执行者检查。但是有的情况是不会对
Thread.interrupt()
进行响应的,例如:服务器套接字的System.in.read()
,所以在遇到DOS攻击或者DDOS攻击时我们没法有效的interrupt/stop
该线程 - 我们在编写多线程程序时,如果我们打算在后续的其他地方处理中断,我们检测到中断后需要重新中断线程保证其传播即可。【因为
Thread.interrupted()
会恢复中断标志位】
Thread.suspend()
,Thread.consume()
的缺点
用途
Thread.suspend()
用来进行对应线程阻塞等待。
Thread.consume()
用来唤醒对应等待的线程。
缺点
Thread.suspend()
有死锁的风险,如果线程调用阻塞前没有释放锁,后面所有试图获得此锁的线程都无法获得锁,当要唤醒此线程的另一个线程的前提条件是获得锁时,死锁就产生了。
转变
我们采用隐式锁完成对应的阻塞操作synchronous
和Object.wait()
,Object.notify()
,这样在阻塞期间会释放此锁,在一定程度上避免死锁。
总结
通过了解上述两种机制转换方式,我们可以很明显的感觉到Java的改善的方向:
相对于让用户通过深入了解API底层机制来减少编码时可能出现的bug,Java在封装API时更倾向于让用户简单、省事的完成API的调用。对于执行速度的优化是要为便于使用这个需求让路的。一如Java最开始提出JVM实现一处编码,处处运行的思路来减少开发成本的意图。
Java 的线程中断机制总结
此处内容来自知乎大佬的博文,原链接参见参考文献。
方法介绍
方法名 | 功能 |
---|---|
Thread.interrupted() |
返回对应线程实例是否被打断, true 表示被打断。同时重置中断标志位为未中断 |
ThreadInstance.isInterrupted() |
返回对应线程实例是否被打断, true 表示被打断 |
ThreadInstance.interrupt() |
中断对应线程实例 |
线程对中断的反应
- RUNNABLE:线程在运行或具备运行条件只是在等待操作系统调度
- WAITING/TIMED_WAITING:线程在等待某个条件或超时【这里指阻塞】
- BLOCKED:线程在等待锁,试图进入同步块【这里指的是竞争锁】
- NEW/TERMINATED:线程还未启动或已结束
RUNNABLE
如果线程在运行中,interrupt()只是会设置线程的中断标志位,没有任何其它作用。线程应该在运行过程中合适的位置检查中断标志位,比如说,如果主体代码是一个循环,可以在循环开始处进行检查,如下所示:
public class InterruptRunnableDemo extends Thread {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
// ... 单次循环代码
}
System.out.println("done ");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new InterruptRunnableDemo();
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
WAITING/TIMED_WAITING
线程执行如下方法会进入WAITING状态:
public final void join() throws InterruptedException
public final void wait() throws InterruptedException
执行如下方法会进入TIMED_WAITING状态:
public final native void wait(long timeout) throws InterruptedException;
public static native void sleep(long millis) throws InterruptedException;
public final synchronized void join(long millis) throws InterruptedException
在这些状态时,对线程对象调用interrupt()会使得该线程抛出InterruptedException,需要注意的是,抛出异常后,中断标志位会被清空(线程的中断标志位会由true重置为false,因为线程为了处理异常已经重新处于就绪状态。),而不是被设置。比如说,执行如下代码:
Thread t = new Thread (){
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//exception被捕获,但是为输出为false 因为标志位会被清空
System.out.println(isInterrupted());
}
}
};
t.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
t.interrupt();//置为true
InterruptedException是一个受检异常,线程必须进行处理。我们在异常处理中介绍过,处理异常的基本思路是,如果你知道怎么处理,就进行处理,如果不知道,就应该向上传递,通常情况下,你不应该做的是,捕获异常然后忽略。
捕获到InterruptedException,通常表示希望结束该线程,线程大概有两种处理方式:
- 向上传递该异常,这使得该方法也变成了一个可中断的方法,需要调用者进行处理
- 有些情况,不能向上传递异常,比如Thread的run方法,它的声明是固定的,不能抛出任何受检异常,这时,应该捕获异常,进行合适的清理操作,清理后,一般应该调用Thread的interrupt方法设置中断标志位,使得其他代码有办法知道它发生了中断
第一种方式的示例代码如下:
//抛出中断异常,由调用者捕获
public void interruptibleMethod() throws InterruptedException{
// ... 包含wait, join 或 sleep 方法
Thread.sleep(1000);
}
第二种方式的示例代码如下:
public class InterruptWaitingDemo extends Thread {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
// 模拟任务代码
Thread.sleep(2000);
} catch (InterruptedException e) {
// ... 清理操作
System.out.println(isInterrupted());//false
// 重设中断标志位为true
Thread.currentThread().interrupt();
}
}
System.out.println(isInterrupted());//true
}
public static void main(String[] args) {
InterruptWaitingDemo thread = new InterruptWaitingDemo();
thread.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
thread.interrupt();
}
}
BLOCKED
BLOCKED 如果线程在等待锁,对线程对象调用interrupt()只是会设置线程的中断标志位,线程依然会处于BLOCKED状态,也就是说,interrupt()并不能使一个在等待锁的线程真正”中断”。我们看段代码:
public class InterruptSynchronizedDemo {
private static Object lock = new Object();//monitor
private static class A extends Thread {
@Override
public void run() {
//等待lock锁
synchronized (lock) {
//等待标志位被置为true
while (!Thread.currentThread().isInterrupted()) {
}
}
System.out.println("exit");
}
}
public static void test() throws InterruptedException {
synchronized (lock) {//获取锁
A a = new A();
a.start();
Thread.sleep(1000);
//a在等待lock锁,interrupt 无法中断
a.interrupt();
//a线程加入当前线程,等待执行完毕
a.join();
}
}
public static void main(String[] args) throws InterruptedException {
test();
}
}
test方法在持有锁lock的情况下启动线程a,而线程a也去尝试获得锁lock,所以会进入锁等待队列,随后test调用线程a的interrupt方法并等待线程线程a结束,线程a会结束吗?不会,interrupt方法只会设置线程的中断标志,而并不会使它从锁等待队列中出来。
我们稍微修改下代码,去掉test方法中的最后一行a.join,即变为:
public static void test() throws InterruptedException {
synchronized (lock) {
A a = new A();
a.start();
Thread.sleep(1000);
a.interrupt();
}
//lock锁释放后 A线程重队列中出来
}
这时,程序就会退出。为什么呢?因为主线程不再等待线程a结束,释放锁lock后,线程a会获得锁,然后检测到发生了中断,所以会退出。
在使用synchronized关键字获取锁的过程中不响应中断请求,这是synchronized的局限性。如果这对程序是一个问题,应该使用显式锁,java中的Lock接口,它支持以响应中断的方式获取锁。对于Lock.lock(),可以改用Lock.lockInterruptibly(),可被中断的加锁操作,它可以抛出中断异常。等同于等待时间无限长的Lock.tryLock(long time, TimeUnit unit)。
NEW/TERMINATE
如果线程尚未启动(NEW),或者已经结束(TERMINATED),则调用interrupt()对它没有任何效果,中断标志位也不会被设置。
IO操作
如果线程在等待IO操作,尤其是网络IO,则会有一些特殊的处理,我们没有介绍过网络,这里只是简单介绍下。
- 实现此InterruptibleChannel接口的通道是可中断的:如果某个线程在可中断通道上因调用某个阻塞的 I/O 操作(常见的操作一般有这些:serverSocketChannel. accept()、socketChannel.connect、socketChannel.open、socketChannel.read、socketChannel.write、fileChannel.read、fileChannel.write)而进入阻塞状态,而另一个线程又调用了该阻塞线程的 interrupt 方法,这将导致该通道被关闭,并且已阻塞线程接将会收到ClosedByInterruptException,并且设置已阻塞线程的中断状态。另外,如果已设置某个线程的中断状态并且它在通道上调用某个阻塞的 I/O 操作,则该通道将关闭并且该线程立即接收到 ClosedByInterruptException;并仍然设置其中断状态。
- 如果线程阻塞于Selector调用,则线程的中断标志位会被设置,同时,阻塞的调用会立即返回。
我们重点介绍另一种情况,InputStream的read调用,该操作是不可中断的,如果流中没有数据,read会阻塞 (但线程状态依然是RUNNABLE),且不响应interrupt(),与synchronized类似,调用interrupt()只会设置线程的中断标志,而不会真正”中断”它,我们看段代码:
public class InterruptReadDemo {
private static class A extends Thread {
@Override
public void run() {
while(!Thread.currentThread().isInterrupted()){
try {
System.out.println(System.in.read())//wait input
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("exit");
}
}
public static void main(String[] args) throws InterruptedException {
A t = new A();
t.start();
Thread.sleep(100);
t.interrupt();
}
}
线程t启动后调用System.in.read()从标准输入读入一个字符,不要输入任何字符,我们会看到,调用interrupt()不会中断read(),线程会一直运行。
不过,有一个办法可以中断read()调用,那就是调用流的close方法,我们将代码改为:
public class InterruptReadDemo {
private static class A extends Thread {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
System.out.println(System.in.read());
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("exit");
}
public void cancel() {
try {
System.in.close();
} catch (IOException e) {
}
interrupt();
}
}
public static void main(String[] args) throws InterruptedException {
A t = new A();
t.start();
Thread.sleep(100);
t.cancel();
}
}
我们给线程定义了一个cancel方法,在该方法中,调用了流的close方法,同时调用了interrupt方法。
如何正确的 取消/关闭 线程
- 以上,我们可以看出,interrupt方法不一定会真正”中断”线程,它只是一种协作机制,如果 不明白线程在做什么,不应该贸然的调用线程的interrupt方法,以为这样就能取消线程。
- 对于以线程提供服务的程序模块而言,它应该封装取消/关闭操作,提供单独的取消/关闭方法给调用者,类似于InterruptReadDemo中演示的cancel方法,外部调用者应该调用这些方法而不是直接调用interrupt。
- Java并发库的一些代码就提供了单独的取消/关闭方法,比如说,Future接口提供了如下方法以取消任务:boolean cancel(boolean mayInterruptIfRunning);
- 再比如,ExecutorService提供了如下两个关闭方法:
void shutdown();
List shutdownNow();
- Future和ExecutorService的API文档对这些方法都进行了详细说明,这是我们应该学习的方式。
Java Throwable
科普
Throwable
相关继承树
Throwable
抛错堆栈信息
在通过构造函数创建异常a时传入异常b,在打印堆栈异常时就可以打印出引起a异常的b的信息,很清晰明显。
同时在后面捕获a时还可以通过代码获得引起a的b的一些message。
参考文献
Java 中一些底层机制转换的原因【可能只是其中一点】
https://docs.oracle.com/javase/6/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html
Java 的线程中断机制总结
https://zhuanlan.zhihu.com/p/27857336