前言:Java中的中断是一种重要的线程控制机制,多用于并发线程编程之中,那么它到底是什么呢?如何工作?和线程切换和阻塞又有什么关系呢?接下来让我们来看看Java中断机制是如何工作的。
线程切换:我们知道,CPU是以时间片进行线程调度的,一个线程在占有一个分配的时间片之后,CPU就会根据相应的策略进行线程的重新调度,这个过程会很大程度上参考线程的优先级,当然调度策略也会考虑到各个线程的等待时间等。也就是说,若是当前线程的优先级足够高的话,那么就有可能在下一次的CPU调度中再次获得一个时间片。若是当前线程未能再次获得时间片,那么它就要插入线程就绪队列,等待CPU的下一次调度,这便是线程之间的切换。
线程阻塞:线程阻塞,指的是当一个线程执行到某一个状态时,这时候它需要获得其他资源才能继续执行(比方说IO资源),但是此时有其他线程占着IO资源不释放,那么这个线程就必须等到其他的线程将IO资源释放之后才能继续执行了,这个便是线程阻塞,此时线程在线程阻塞队列而非就绪队列中。Java中的sleep()会引起线程阻塞。(yield()-不会阻塞,仅仅是重新调度,wait()-挂起)
线程中断:汇编语言中的中断一般指暂停当前的程序,然后跳到中断入口,执行相应的中断处理程序,处理完毕之后回到之前程序的断点继续执行。那么Java中的中断是不是也是指停止当前程序运行的意思呢?可能会觉得会奇怪,其实并非是这样的。它的存在可以说是给我们提供了一种线程的控制机制。线程中断它指的并不只是等到线程到达某个检查点决定的中断,还包括有些时候在无法到达检查点,我们需要在run()方法中执行中断。接下来让我们走近中断。
public class InterruptTest {
public static void main(String args[]) throws InterruptedException {
Thread thread = new Thread(new NonBlockedTest());
thread.start();
Thread.sleep(50);
System.out.println("接下来中断线程");
thread.interrupt();
}
/**
* 没有阻塞操作的线程
*
*/
private static class NonBlockedTest implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("线程执行中...");
}
}
}
}
这段程序很好理解,启动没有阻塞操作的线程,让主线程休眠50ms之后,对这个被启动的线程执行中断,我们发现,若非你自己强制关闭这个进程,这个程序会陷入死循环之中。根本不会退出来,也就是说Java中为我们提供的中断方法interrupt()并不能直接停止线程的执行。
查阅api,是这么说的,其实interrupt()方法仅仅是为我们设置了线程的中断标志,那么我们是否可以按照这个思路对线程进行“真正意义上的”中断呢?答案是可以的,Java还为我们提供了interrupted()方法检查中断标志。将上诉代码稍作修改,我们再看看结果,可以发现的确可以正常停止了。
private static class NonBlockedTest implements Runnable {
@Override
public void run() {
while (!Thread.interrupted()) {
System.out.println("线程执行中...");
}
}
}
public class InterruptTest {
public static void main(String args[]) throws InterruptedException {
Thread thread = new Thread(new NonBlockedTest());
thread.start();
Thread.sleep(1000);
System.out.println("接下来中断线程");
thread.interrupt();
}
/**
* 有阻塞操作的线程
*
*/
private static class NonBlockedTest implements Runnable {
@Override
public void run() {
try {
System.out.println("线程开始阻塞调用");
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println("InterruptedExceptioon");
}
System.out.println("Exit run()");
}
}
}
写代码的时候我们发现,我们没办法避免一个问题,就是当我们调用sleep()方法的时候(这是一个会导致线程阻塞的操作),我们必须处理InterruptedException异常,这个异常属于Java特有的check异常,因此我们没办法放之不管。事实上,我们不能够也没必要强行在try-catch语句外面加上while(!Thread.interrupted())这样的检查(这样会陷入死循环),原因下面会有解释。
public class InterruptTest implements Runnable {
private volatile double d = 0.0;
public static void main(String args[]) throws Exception {
// 传入主线程睡眠时间
if (args.length != 1) {
System.out.println("usage:java InterruptingIdiom delay-in-ms");
System.exit(1);
}
Thread t = new Thread(new InterruptTest());
t.start();
TimeUnit.MILLISECONDS.sleep(Integer.parseInt(args[0]));
t.interrupt();
}
@Override
public void run() {
try {
while (!Thread.interrupted()) {
// point1
NeedsCleanup n1 = new NeedsCleanup(1);
try {
System.out.println("Sleeping");
TimeUnit.SECONDS.sleep(1);
// point2
NeedsCleanup n2 = new NeedsCleanup(2);
try {
System.out.println("Calculating");
for (int i = 0; i < 2500000; i++) {
d = d + (Math.PI + Math.E) / d;
}
System.out.println("Finished time-consuming operation");
} finally {
n2.cleanup();
}
} finally {
n1.cleanup();
}
}// end of while
System.out.println("Exiting while()");
} catch (InterruptedException e) {
System.out.println("Exiting via InterruptedException");
}
}
}
class NeedsCleanup {
private final int id;
public NeedsCleanup(int id) {
this.id = id;
}
public void cleanup() {
System.out.println("Cleaning up " + id);
}
}
当参数为900(0.9s)的时候,也就是主线程会比子线程先被唤醒,这时候主线程调用子线程的interrupt()方法的时候,子线程还处于阻塞状态,那么程序会抛出中断异常,并且重置中断状态,对应于中断发生在while语句和point1之间,这时候程序需要回收n1对象;
若是我把参数调到了1050(不同机器会有差别),也时候主线程发出中断信号的时候。子线程刚好处于那个循环的耗时操作中,我们可以发现,子线程不会立即终止,而是继续执行完for循环,就像前面说的,interrupt()不会中断线程,这是需要自行检查并执行的。那么这种情况下,对应于代码中point1和point2之间,程序会依次回收n2和n1对象,并且在下一次while检查的时候检测出中断标志并且退出,重置中断标志。通过这个例子,我们了解到了在中断的时候正确处理的一些技巧和资源回收的必要性。