Java没有提供任何的安全机制来终止线程,程,虽然Thread.stop和suspend等方法提供了这样的机制,但是存在严重的缺陷,应该避免使用这些方法。
它提供了中断(interrupt),这是一种协作机制,能够使一个线程终止另一个线程当前的工作。
一种协作机制是设置一个“已请求取消(Cancellation Requested)”标志,而任务定期查看该标志。
“已请求取消”标志
private volatile boolean cancelled;
public void run() {
while (!cancelled) {
doSomething();
}
}
public void cancel() {
cancelled = true;
}
这种方式通过轮询该标志确定是否继续执行该任务。如果该标志cancel未被调用,那么该任务将一直运行下去。Volatile保证了标志的可见性,使得这个过程可以可靠地工作。
Cancel的取消机制可以使该任务退出,但在退出过程中需要花费一定的时间。极端情况下当任务调用了一个阻塞方法,比如sleep/wait/…那么任务可能永远不会检查取消标志,因此永远不会结束。
public class CloseThread extends Thread {
private volatile boolean cancelled;
public void run() {
while (!cancelled) {
doSomething();
}
}
private void doSomething() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void cancel() {
cancelled = true;
}
public static void main(String[] args) {
final CloseThread closeThread = new CloseThread();
closeThread.start();
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
closeThread.cancel();
System.out.println("closeThread isAlive --" + closeThread.isAlive());
}
}, 2000);
}
}
打印如下:
closeThread isAlive –true
在run方法里面轮询状态标志,当cancelled为false时就继续执行任务,在2S之后调用cancel方法将cancelled标志置为true。
可以看到当在run方法里面耗时比较长的时,每10S才能轮询一次,即退出任务需要花费一定的时间,此时调用并不能关闭线程,在10S后线程才能关闭。极限情况下,即遇到阻塞方法时,线程永远关闭不了。
因此这种设置标志的方式存在问题,自定义的取消机制无法与可阻塞的方法实现良好的交互。
使用中断关闭线程
通常,中断是实现取消的最合理方式。
中断是一种协作机制,线程可以通过这种机制来通知另一个线程,告诉他再合适的或者可能的情况下停止当前工作,并转而执行其他的工作。
public class Thread{
public void interrupt() { ... } //中断一个线程
public boolean isInterrupted() { ... } //获取中断标志,判断是否中断
public static boolean interrupted() { ... } //清除中断状态,并返回它之前的状态值
}
每个线程都有一个boolean类型的中断状态。当中断线程时,这个线程的中断状态将被设置成true。
调用interrupt并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息,换句话说,仅仅修改了线程的标志字段。
对中断操作的正确理解是:他并不会真正的中断一个正在运行的线程,而只是发出了中断请求,然后由线程在下一个合适的时刻(取消点)中断自己。比如sleep/wait/join等将严格的处理这种请求。当他们收到中断请求或者在开始执行的时候发现某个已被设置好的中断状态时,将抛出一个异常。
使用中断取消线程任务分三种情况
- 非阻塞状态下发生中断
- 阻塞状态下可中断的阻塞
- 阻塞状态下不可中断的阻塞
非阻塞状态下发生中断
当线程在非阻塞状态下中断的时候,它的中断状态将被设置,如果不触发InterruptedException,那么中断状态将一直保持,然后根据检查中断状态来判断是否发生了中断。
可以通过在任务代码中轮询当前线程的中断状态,要选择合适的轮询频率,就需要在效率和响应性之间权衡。
阻塞状态下可中断的阻塞
阻塞库函数中断
当wait/sleep/BlockingQueue.put等阻塞库的方法会在发现中断时提前返回,它们在中断的时候执行的操作包括:清除中断状态,抛出InterruptException。
public void run() {
while (!Thread.currentThread().isInterrupted()) {
doSomething();
}
}
private void doSomething() {
try {
Thread.sleep(100 * 1000);
// 阻塞操作
} catch (InterruptedException e) {
// ?????
// 要有对应的中断响应
}
}
public void cancel() {
interrupt();
}
while里面显式的检测对中断具有更高的响应性。
如何响应中断?
响应中断的方式
1. 传递异常,从而使调用方法也成为可中断的阻塞方法。简单说就是添加抛出异常的声明。
2. 恢复中断状态,从而使调用栈的上层代码能够对其进行处理。标准的做法就是在catch里面再次调用interrupt来恢复中断状态,从而让上层代码能够对其进行处理。
上面的示例中在catch里面可以再次调用interrupt来恢复中断状态,当再次while循环时无法满足条件就会退出,考虑下面的示例
当捕获到InterruptException时除非在代码里实现了中断策略才可以屏蔽中断。中断策略指的是收到中断请求之后该怎么做?比如下面的中断策略就是线程会马上退出,没有上层代码需要知道中断信息。
// InterruptException中屏蔽中断,是因为他已经知道线程将要结束了
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
Thread.sleep(10 * 1000);
}
} catch (InterruptedException e) {
/* 允许线程退出 */
}
}
public void cancel() {
interrupt();
}
不支持取消但仍可调用可中断阻塞方法的情况
不支持取消但仍可调用可中断阻塞方法的操作。当catch到中断时应该本地保存中断状态,并在返回前恢复中断状态而不要在捕获到中断状态时恢复,如果过早的设置中断状态有可能引起无限循环,因为大多数可中断的阻塞方法在入口处检查中断状态,并且当发现该状态已被设置时立即抛出InterruptException
public Task getNextTask(BlockingQueue<Task> queue) {
boolean interrupted = false;
try {
while (true) {
try {
return queue.take();
} catch (InterruptedException e) {
interrupted = true;
}
}
} finally {
if (interrupted)
Thread.currentThread().interrupt();
}
}
阻塞状态下不可中断的阻塞
许多阻塞的方法都是通过提前返回或者抛出InterruptException来响应中断请求的,从而使开发人员能容易的构建出能取消请求的任务,然而并非所有的可儿阻塞方法或者阻塞机制都能响应中断。比如socket I/O、等待获取内置锁,中断请求只能设置线程的中断状态,除此之外没有其他任何作用。我们可以使用类似中断的手段来停止这些线程,但这要求知道线程阻塞的原因。
同步Socket I/O 虽然InputStream和OutputStream中的read和write方法都不会想要中断,但通过关闭底层套接字,可以使得由于执行read和write等方法而被阻塞的线程抛出一个SocketException。也就是在需要取消任务的地方关闭底层套接字,在SocketException里面就可以响应中断。
public class CloseSocket extends Thread {
private Socket socket;
private InputStream in;
public CloseSocket(Socket socket) throws IOException {
this.socket = socket;
this.in = socket.getInputStream();
}
@Override
public void interrupt() {
try {
// Closing this socket will also close the socket's InputStream and OutputStream.
socket.close();
} catch (IOException e) {
} finally{
super.interrupt();
}
}
@Override
public void run() {
try {
byte [] buf = new byte[1024];
while (true) {
int num = in.read(buf);
// ......
}
} catch (IOException e) {
// 线程直接退出好了
}
}
}
改写了interrupt方法,使其技能处理标准的中断也能关闭底层套接字,因此无论线程是在read方法中阻塞还是在某个可中断的阻塞方法中阻塞,都可以被中断并停止当前的工作。
关于Thread.stop和suspend不建议使用 Java Thread Primitive Deprecation