Java基础知识---线程的中断

前言: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("线程执行中...");
			}
		}
	}

Java基础知识---线程的中断_第1张图片



三、第二个中断示例(阻塞线程)

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()");
		}
	}

}
Java基础知识---线程的中断_第2张图片

写代码的时候我们发现,我们没办法避免一个问题,就是当我们调用sleep()方法的时候(这是一个会导致线程阻塞的操作),我们必须处理InterruptedException异常,这个异常属于Java特有的check异常,因此我们没办法放之不管。事实上,我们不能够也没必要强行在try-catch语句外面加上while(!Thread.interrupted())这样的检查(这样会陷入死循环),原因下面会有解释。


四、Java中线程中断的工作原理

看了上面两个示例之后,你可能会有很多小疑问,究竟什么时候我们该用interrupted(),什么时候我们该用异常?这两种处理又是表示什么意义。
事实上,上面也提到了,interrupt()方法不能直接中断线程,而是为其设置一个中断标志。对于非阻塞任务,我们可以调用interrupted()方法来检查interrupt()方法是否被调用过(然后便可以进行自定义中断处理),并且这个方法还会将中断标志清空掉,这样也就保证了中断处理只进行一次。对于阻塞任务,我们就要通过抛出InterruptedException进行处理了,同样地,这个异常抛出的同时会重置中断标志。因此当任务较为复杂的时候,我们需要谨慎处理,保证中断经由单一的异常或是interrupted()处理掉。
因此上面的第二个示例,同时使用两种检查中断的方法显然是不对的,异常抛出后线程的中断状态已经被重置了,此时while检查出的结果依旧是满足条件了,因此会进入死循环中。

附1:我们发现,当我们使用interrupt()方法的时候,我们必须持有该线程的引用。同时,新的concurrent类库似乎在避免我们对Thread对象的直接操作,转而尽量通过Executor进行操作。对于Executor(线程池)来说,若是我们调用其 shutdownNow(),那么Executor会向其中所有的线程发送interrupt()消息。但是也有的时候我们需要对线程池中的单个线程进行操作,这时候我们可以使用submit()而非execute()提交任务,这样就可以返回一个Future对象,通过其cancel()方法就可以中断单个任务了。
附2:并非所有的阻塞操作都是可以被中断的。比方说IO资源上的阻塞和synchronized同步块上的阻塞都是不可以中断的。具体可以自行验证。那么这种情况下若是要中断线程,那么我们只能粗暴地关闭底层资源了,如inputStream.close()。

五、一个较为复杂的中断示例

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检查的时候检测出中断标志并且退出,重置中断标志。通过这个例子,我们了解到了在中断的时候正确处理的一些技巧和资源回收的必要性。


六、一句总结性的概括

若是我们调用线程的中断方法,当程序即将进入或是已经进入阻塞调用的时候,那么这个中断信号应该由InterruptedException捕获并进行重置;当run()方法程序段中不会出现阻塞操作的时候,这时候中断并不会抛出异常,我们需要通过interrupted()方法进行中断检查和中断标志的重置。另外,知道IO操作和synchronized上的阻塞不可中断也是必要的。


你可能感兴趣的:(Java)