Java编程的逻辑 (69) - 线程的中断
本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http://item.jd.com/12299018.html

本节主要讨论一个问题,如何在Java中取消或关闭一个线程?
取消/关闭的场景
我们知道,通过线程的start方法启动一个线程后,线程开始执行run方法,run方法运行结束后线程退出,那为什么还需要结束一个线程呢?有多种情况,比如说:
- 很多线程的运行模式是死循环,比如在生产者/消费者模式中,消费者主体就是一个死循环,它不停的从队列中接受任务,执行任务,在停止程序时,我们需要一种"优雅"的方法以关闭该线程。
- 在一些图形用户界面程序中,线程是用户启动的,完成一些任务,比如从远程服务器上下载一个文件,在下载过程中,用户可能会希望取消该任务。
- 在一些场景中,比如从第三方服务器查询一个结果,我们希望在限定的时间内得到结果,如果得不到,我们会希望取消该任务。
- 有时,我们会启动多个线程做同一件事,比如类似抢火车票,我们可能会让多个好友帮忙从多个渠道买火车票,只要有一个渠道买到了,我们会通知取消其他渠道。
取消/关闭的机制
Java的Thread类定义了如下方法:
public final void stop()
这个方法看上去就可以停止线程,但这个方法被标记为了过时,简单的说,我们不应该使用它,可以忽略它。
在Java中,停止一个线程的主要机制是中断,中断并不是强迫终止一个线程,它是一种协作机制,是给线程传递一个取消信号,但是由线程来决定如何以及何时退出,本节我们主要就是来理解Java的中断机制。
Thread类定义了如下关于中断的方法:
public boolean isInterrupted()
public void interrupt()
public static boolean interrupted()
这三个方法名字类似,比较容易混淆,我们解释一下。isInterrupted()和interrupt()是实例方法,调用它们需要通过线程对象,interrupted()是静态方法,实际会调用Thread.currentThread()操作当前线程。
每个线程都有一个标志位,表示该线程是否被中断了。
- isInterrupted:就是返回对应线程的中断标志位是否为true。
- interrupted:返回当前线程的中断标志位是否为true,但它还有一个重要的副作用,就是清空中断标志位,也就是说,连续两次调用interrupted(),第一次返回的结果为true,第二次一般就是false (除非同时又发生了一次中断)。
- interrupt:表示中断对应的线程,中断具体意味着什么呢?下面我们进一步来说明。
线程对中断的反应
interrupt()对线程的影响与线程的状态和在进行的IO操作有关,我们先主要考虑线程的状态:
- RUNNABLE:线程在运行或具备运行条件只是在等待操作系统调度
- WAITING/TIMED_WAITING:线程在等待某个条件或超时
- BLOCKED:线程在等待锁,试图进入同步块
- NEW/TERMINATED:线程还未启动或已结束
RUNNABLE
如果线程在运行中,且没有执行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/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,需要注意的是,抛出异常后,中断标志位会被清空,而不是被设置。比如说,执行如下代码:
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,通常表示希望结束该线程,线程大概有两种处理方式:
- 向上传递该异常,这使得该方法也变成了一个可中断的方法,需要调用者进行处理。
- 有些情况,不能向上传递异常,比如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) {
// ... 清理操作
// 重设中断标志位
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();
}
}
BLOCKED
如果线程在等待锁,对线程对象调用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/TERMINATE
如果线程尚未启动(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操作,尤其是网络IO,则会有一些特殊的处理,我们没有介绍过网络,这里只是简单介绍下。
- 如果IO通道是可中断的,即实现了InterruptibleChannel接口,则线程的中断标志位会被设置,同时,线程会收到异常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());
} 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操作开始。
(与其他章节一样,本节所有代码位于 https://github.com/swiftma/program-logic)
----------------
未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索Java编程及计算机技术的本质。用心原创,保留所有版权。

Java编程的逻辑 (69) - 线程的中断的更多相关文章
- Java编程的逻辑 (65) - 线程的基本概念
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
- Java编程的逻辑 (67) - 线程的基本协作机制 (上)
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
- Java编程的逻辑 (78) - 线程池
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
- Java编程的逻辑 (68) - 线程的基本协作机制 (下)
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
- 《Java编程的逻辑》 - 文章列表
<计算机程序的思维逻辑>系列文章已整理成书<Java编程的逻辑>,由机械工业出版社出版,2018年1月上市,各大网店有售,敬请关注! 京东自营链接:https://item.j ...
- Java编程的逻辑 (83) - 并发总结
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
- Java编程的逻辑 (77) - 异步任务执行服务
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
- Java编程的逻辑 (81) - 并发同步协作工具
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
- Java编程的逻辑 (34) - 随机
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...
随机推荐
- C#快速删除bin和obj文件夹的方法
C#程序总会生成bin和obj文件夹,为了减小源码的大小,就有必要将这两个文件夹删除,于是想到用批处理文件来删除. 以下是批处理的代码: @echo offset nowPath=%cd%cd /cd ...
- MT【85】正整数系数
评:这类与正整数有关的题,是很多学生所不习惯以及无从下手的.事实上很多时候要用到整数的这个性质:$m>n,m,n\in Z$则$m\ge n+1$,这道题用二次函数区间上有根的一般做法也可以,大 ...
- 【Revit API】墙体分割
Revit中没有分割墙体的直接方法,一般采用调整原墙体的长度,再复制自身,调整复制体的长度.话不多说,直接上代码 public static void WallCopy(Document doc, X ...
- BZOJ2322 [BeiJing2011]梦想封印 【set + 线性基】
题目链接 BZOJ2322 题解 鉴于BZOJ2115,要完成此题,就简单得多了 对图做一遍\(dfs\),形成\(dfs\)树,从根到每个点的路径形成一个权值,而每个返祖边形成一个环 我们从根出发去 ...
- zabbix使用percona插件监控mysql
1.添加percona仓库. # yum install -y http://www.percona.com/downloads/percona-release/redhat/0.1-4/percon ...
- (转)Maven学习总结(三)——使用Maven构建项目
孤傲苍狼 只为成功找方法,不为失败找借口! Maven学习总结(三)——使用Maven构建项目 maven作为一个高度自动化构建工具,本身提供了构建项目的功能,下面就来体验一下使用maven构建项目的 ...
- RBAC: 基于角色的访问控制(Role-Based Access Control)
本文只讨论两种基于角色的访问控制的不同点,不涉及权限设计的数据库设计. 基于角色的访问控制(Role-Based Access Control)可分为隐式角色访问控制和显式角色访问控制. 隐式角色访问 ...
- 数组指针与指针数组(good)
数组指针和指针数组的区别 数组指针(也称行指针) 定义 int (*p)[n]; ()优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长.也就是说执行p ...
- SQL Server 排名函数( ROW_NUMBER、RANK、DENSE_RANK、NTILE )
排名函数是Sql Server2005新增的功能,下面简单介绍一下他们各自的用法和区别.我们新建一张Order表并添加一些初始数据方便我们查看效果. CREATE TABLE [dbo].[Order ...
- Flask-SQLAlchemy 无法创建Sqlite 数据库???
<Flask web 开发>第五章数据库照书中的方法无法创建表,没有python的命令提示符,只是运行程序输入db.create_all()后文件夹里也没有data.sqlite文件 解决 ...