前面介绍过了几种线程状态和几种状态之间的转换白话java锁–线程状态。此篇文章主要介绍的是对线程中断的理解。其实我一直不太理解为什么中断的时候线程会抛出个InterruptedException异常。
线程中断API
在以前的版本中使用stop()方法中断线程,但是该方法已经废弃了
@Deprecated
public final void stop() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
checkAccess();
if (this != Thread.currentThread()) {
security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);
}
}
// A zero status value corresponds to "NEW", it can't change to
// not-NEW because we hold the lock.
if (threadStatus != 0) {
resume(); // Wake up thread if it was suspended; no-op otherwise
}
// The VM can handle all thread states
stop0(new ThreadDeath());
}
现在的线程终止使用Thread类中的如下方法:
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
public boolean isInterrupted() {
return isInterrupted(false);
}
总共分为三个方法,主要就是interrupt()方法,下面详细讲解一下这个方法
interrupt()
一步一步看interrupt()方法的实现
if (this != Thread.currentThread())
checkAccess();
首先判断调用interrupt()方法的线程是否是当前线程(一个线程当然可以中断自己),如果另一个线程想要中断当前线程,需要调用checkAccess()方法进行权限校验。
public final void checkAccess() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkAccess(this);
}
}
checkAccess()方法里面的实现就是根据是否启用了SecurityManager来判断线程权限(启动的方式就是在jvm启动参数加上-Djava.security.manager,根据配置文件的配置校验权限)
interrupt0();
先看这个方法,这个方法的实现是:
private native void interrupt0();
是一个native方法,是通过JNI调用的c语言的方法,但是通过注释
// Just to set the interrupt flag
我们可以知道,调用这个方法只是设置了interrupt的标识。那么什么是interrupt标识呢?我们再读一段注释:
* <p> If this thread is blocked in an invocation of the {@link
* Object#wait() wait()}, {@link Object#wait(long) wait(long)}, or {@link
* Object#wait(long, int) wait(long, int)} methods of the {@link Object}
* class, or of the {@link #join()}, {@link #join(long)}, {@link
* #join(long, int)}, {@link #sleep(long)}, or {@link #sleep(long, int)},
* methods of this class, then its interrupt status will be cleared and it
* will receive an {@link InterruptedException}.
这段注释的主要意思是说:如果线程被阻塞在了
Object的
- wait()
- wait(long)
- wait(long,int)
Thread的
- join()
- join(long)
- join(long,int)
- sleep(long)
- sleep(long,int)
然后他的interrupt状态会被清除,并且会得到一个InterruptedException异常(这里终于提到这个异常了)。
对于上面的话可能会懵,首先解释一下什么叫interrupt状态(interrupt标识)。通过一个简单的例子就可以很好理解:
private volatile boolean cancelled;
public void run() {
//如果取消,则退出
while (!cancelled) {
//do something
}
}
public void cancel() {
cancelled = true;
}
给执行run()方法中设置一个标识,然后再调用cancel()方法将标识位设置为取消,那么run()中的方法就自然退出了。通过这个例子就可以了解到其实interrupt标识就是上面例子中的while判断标识。
实际上在执行interrupt0()方法的时候底层的原理就是和上面的例子一样,线程在调用interrupt0()方法的时候将interrupt标识设置为true,线程会不断的判断这个interrupt标识,来决定线程是否应该被中断。所以这就是和stop()方法的不同,stop()方法是不管代码执行到哪里都会直接将线程停止,但是使用interrupt标识的话,就可以由程序代码决定线程终止之后的行为。
那么在了解了interrupt标识之后,我们再解释一下为什么在调用interrupt()方法的时候,如果线程因为调用这些sleep()、join()、wait()方法而阻塞的时候,interrupt状态会被清除,然后抛出InterruptedException异常。通过上一节线程状态可以知道,当调用完这些方法之后,线程会处于等待状态(等待其他线程调用notify()方法或者notifyAll()方法),而这个线程自己则会因为等待某个条件而一动不动,所以要想让这种线程停止(并且不使用唤醒方法),在java中的办法就是让这个阻塞线程自己抛出异常,从而使线程退出阻塞状态,通过try/catch捕获到异常,知道这个线程因为阻塞而中断了,后续可以定制异常处理。
再看interrupt()方法的下面一段
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
此段代码主要针对的是IO/NIO的操作
* <p> If this thread is blocked in an I/O operation upon an {@link
* java.nio.channels.InterruptibleChannel InterruptibleChannel}
* then the channel will be closed, the thread's interrupt
* status will be set, and the thread will receive a {@link
* java.nio.channels.ClosedByInterruptException}.
* * <p> If this thread is blocked in a {@link java.nio.channels.Selector}
* then the thread's interrupt status will be set and it will return
* immediately from the selection operation, possibly with a non-zero
* value, just as if the selector's {@link
* java.nio.channels.Selector#wakeup wakeup} method were invoked.
通过注释可以了解到:
- 如果一个线程阻塞在了I/O操作上,并且这个I/O是实现了InterruptibleChannel接口的,如果这个通道要关闭,线程的中断状态会被设置,并且会收到ClosedByInterruptException异常
- 如果一个线程阻塞在了Selector上,会设置这个线程的中断状态并且立即从selection操作中返回,可能是个空值,就像调用了wakeup()方法一样。如果此时再次调用执行wakeup()方法则会抛出ClosedSelectorException异常
其实上面的两段话和抛出InterruptedException异常的想法有点类似,都是如果因为IO问题而阻塞的时候,如果需要关闭这个线程,这个线程会抛出异常,代码里面处理后续关闭异常逻辑即可。
总结
稍微总结一下java对于中断的处理,其实就分为两步
第一步就是给所有线程一个标志位,然后这个线程不停的去判断这个标志位是否改变,如果想要终止这个线程就改变这个标志位,然后线程知道标志位改变了,执行后续的终止逻辑
第二步就是如果这个线程因为IO、网络原因或者处于等待锁的状态时,这个线程不能继续往下进行了,如果中断这个线程,这个线程会立即从阻塞状态中退出,并抛出InterruptedException异常,告诉上层我退出了,后续再做退出后的处理
所以无论是哪一步都是给予这个线程的建议,当然也可以忽略(简单的try/catch),继续执行,好像什么都没有发生,所以在java中,中断只是传递了一个请求中断的请求,并不会直接阻止一个线程的运行。
扩展
在这里扩展一下线程每种状态下如果中断会对他们造成什么影响
- NEW/TERMINATED : 如果此时调用中断方法是毫无意义的,因为线程还未真正启动,而TERMINATED则表示线程已经死亡,所以不会设置中断标识位,而且什么事都不会发生
- RUNNABLE : 处于这种状态的线程不一定会获取CPU执行权限,因为在一个时间段里面,CPU只能执行一个线程,其他线程虽然是RUNNABLE状态,但是没有获得CPU执行权限。如果中断这种类型的线程只会设置这个线程的中断标志位,不会实际中断,线程会继续运行,中断应该由我们程序控制,而不是交给系统强制停止。给了程序很大的灵活性
- BLOCKED : 这种状态和RUNNABLE状态的线程中断效果类似,这种线程只是因为某个对象锁而阻塞了,但是还是有竞争机会的,还是有可执行权的,所以这种类型的线程能够继续运行判断中断标识位的状态
- WAITING/TIMED_WAITING : 处于这种状态的线程在中断的时候就是上面我所说的抛出InterruptedException异常的情况,在抛出异常的同时会清空中断标志位(也就是还原中断标识位为可中断状态),后续可以通过程序重新设置
综上所述,NEW/TERMINATED对于中断是没有反应的,因为也没有意义;RUNNABLE/BLOCKED对于中断只是设置的标识位,并不是强制中断,终止的权限还是在程序手中的;WAITING/TIMED_WAITING对于中断的反应是强烈的,会抛出异常并重置中断标识