本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》(马俊昌著),由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买:京东自营连接 html
![]()
本节主要讨论一个问题,如何在Java中取消或关闭一个线程?java
咱们知道,经过线程的start方法启动一个线程后,线程开始执行run方法,run方法运行结束后线程退出,那为何还须要结束一个线程呢?有多种状况,好比说:git
Java的Thread类定义了以下方法:github
public final void stop() 复制代码
这个方法看上去就能够中止线程,但这个方法被标记为了过期,简单的说,咱们不该该使用它,能够忽略它。编程
在Java中,中止一个线程的主要机制是中断,中断并非强迫终止一个线程,它是一种协做机制,是给线程传递一个取消信号,可是由线程来决定如何以及什么时候退出,本节咱们主要就是来理解Java的中断机制。swift
Thread类定义了以下关于中断的方法:bash
public boolean isInterrupted() public void interrupt() public static boolean interrupted() 复制代码
这三个方法名字相似,比较容易混淆,咱们解释一下。isInterrupted()和interrupt()是实例方法,调用它们须要经过线程对象,interrupted()是静态方法,实际会调用Thread.currentThread()操做当前线程。服务器
每一个线程都有一个标志位,表示该线程是否被中断了。微信
interrupt()对线程的影响与线程的状态和在进行的IO操做有关,咱们先主要考虑线程的状态:网络
若是线程在运行中,且没有执行IO操做,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状态:
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,须要注意的是,抛出异常后,中断标志位会被清空,而不是被设置。好比说,执行以下代码:
Thread t = new Thread (){
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(isInterrupted());
}
}
};
t.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
t.interrupt();
复制代码
程序的输出为false。
InterruptedException是一个受检异常,线程必须进行处理。咱们在异常处理中介绍过,处理异常的基本思路是,若是你知道怎么处理,就进行处理,若是不知道,就应该向上传递,一般状况下,你不该该作的是,捕获异常而后忽略。
捕获到InterruptedException,一般表示但愿结束该线程,线程大概有两种处理方式:
第一种方式的示例代码以下:
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) {
// ... 清理操做
// 重设中断标志位
Thread.currentThread().interrupt();
}
}
System.out.println(isInterrupted());
}
public static void main(String[] args) {
InterruptWaitingDemo thread = new InterruptWaitingDemo();
thread.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
thread.interrupt();
}
}
复制代码
若是线程在等待锁,对线程对象调用interrupt()只是会设置线程的中断标志位,线程依然会处于BLOCKED状态,也就是说,interrupt()并不能使一个在等待锁的线程真正"中断"。咱们看段代码:
public class InterruptSynchronizedDemo {
private static Object lock = new Object();
private static class A extends Thread {
@Override
public void run() {
synchronized (lock) {
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.interrupt();
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();
}
}
复制代码
这时,程序就会退出。为何呢?由于主线程再也不等待线程a结束,释放锁lock后,线程a会得到锁,而后检测到发生了中断,因此会退出。
在使用synchronized关键字获取锁的过程当中不响应中断请求,这是synchronized的局限性。若是这对程序是一个问题,应该使用显式锁,后面章节咱们会介绍显式锁Lock接口,它支持以响应中断的方式获取锁。
若是线程还没有启动(NEW),或者已经结束(TERMINATED),则调用interrupt()对它没有任何效果,中断标志位也不会被设置。好比说,如下代码的输出都是false。
public class InterruptNotAliveDemo {
private static class A extends Thread {
@Override
public void run() {
}
}
public static void test() throws InterruptedException {
A a = new A();
a.interrupt();
System.out.println(a.isInterrupted());
a.start();
Thread.sleep(100);
a.interrupt();
System.out.println(a.isInterrupted());
}
public static void main(String[] args) throws InterruptedException {
test();
}
}
复制代码
若是线程在等待IO操做,尤为是网络IO,则会有一些特殊的处理,咱们没有介绍过网络,这里只是简单介绍下。
咱们重点介绍另外一种状况,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());
} 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方法,此次,程序会输出:
-1
exit
复制代码
也就是说,调用close方法后,read方法会返回,返回值为-1,表示流结束。
以上,咱们能够看出,interrupt方法不必定会真正"中断"线程,它只是一种协做机制,若是不明白线程在作什么,不该该贸然的调用线程的interrupt方法,觉得这样就能取消线程。
对于以线程提供服务的程序模块而言,它应该封装取消/关闭操做,提供单独的取消/关闭方法给调用者,相似于InterruptReadDemo中演示的cancel方法,外部调用者应该调用这些方法而不是直接调用interrupt。
Java并发库的一些代码就提供了单独的取消/关闭方法,好比说,Future接口提供了以下方法以取消任务:
boolean cancel(boolean mayInterruptIfRunning);
复制代码
再好比,ExecutorService提供了以下两个关闭方法:
void shutdown();
List<Runnable> shutdownNow();
复制代码
Future和ExecutorService的API文档对这些方法都进行了详细说明,这是咱们应该学习的方式。关于这两个接口,咱们后续章节介绍。
本节主要介绍了在Java中如何取消/关闭线程,主要依赖的技术是中断,但它是一种协做机制,不会强迫终止线程,咱们介绍了线程在不一样状态和IO操做时对中断的反应,做为线程的实现者,应该提供明确的取消/关闭方法,并用文档描述清楚其行为,做为线程的调用者,应该使用其取消/关闭方法,而不是贸然调用interrupt。
从65节到本节,咱们介绍的都是关于线程的基本内容,在Java中还有一套并发工具包,位于包java.util.concurrent下,里面包括不少易用且高性能的并发开发工具,从下一节开始,咱们就来讨论它,先从最基本的原子变量和CAS操做开始。
(与其余章节同样,本节全部代码位于 github.com/swiftma/pro…)
未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),深刻浅出,老马和你一块儿探索Java编程及计算机技术的本质。用心原创,保留全部版权。