Java基礎知識---線程的中斷

前言:Java中的中斷是一種重要的線程控制機制,多用於併發線程編程之中,那麼它到底是什麼呢?如何工作?和線程切換和阻塞又有什麼關係呢?接下來讓我們來看看Java中斷機制是如何工作的。

一、什麼是線程切換,線程阻塞,線程中斷?

線程切換:我們知道,CPU是以時間片進行線程調度的,一個線程在佔有一個分配的時間片之後,CPU就會根據相應的策略進行線程的重新調度,這個過程會很大程度上參考線程的優先級,當然調度策略也會考慮到各個線程的等待時間等。也就是說,若是當前線程的優先級足夠高的話,那麼就有可能在下一次的CPU調度中再次獲得一個時間片。若是當前線程未能再次獲得時間片,那麼它就要插入線程就緒隊列,等待CPU的下一次調度,這便是線程之間的切換。

線程阻塞:線程阻塞,指的是當一個線程執行到某一個狀態時,這時候它需要獲得其他資源才能繼續執行(比方說IO資源),但是此時有其他線程佔着IO資源不釋放,那麼這個線程就必須等到其他的線程將IO資源釋放之後才能繼續執行了,這個便是線程阻塞,此時線程在線程阻塞隊列而非就緒隊列中。Java中的sleep()會引起線程阻塞。(yield()-不會阻塞,僅僅是重新調度,wait()-掛起)

線程中斷:彙編語言中的中斷一般指暫停當前的程序,然後跳到中斷入口,執行相應的中斷處理程序,處理完畢之後回到之前程序的斷點繼續執行。那麼Java中的中斷是不是也是指停止當前程序運行的意思呢?可能會覺得會奇怪,其實並非是這樣的。它的存在可以說是給我們提供了一種線程的控制機制。線程中斷它指的並不只是等到線程到達某個檢查點決定的中斷,還包括有些時候在無法到達檢查點,我們需要在run()方法中執行中斷。接下來讓我們走近中斷。

二、第一個中斷示例(非阻塞線程)

<span style="font-size:14px;">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("線程執行中...");
			}
		}
	}
 
}</span>

這段程序很好理解,啓動沒有阻塞操作的線程,讓主線程休眠50ms之後,對這個被啓動的線程執行中斷,我們發現,若非你自己強制關閉這個進程,這個程序會陷入死循環之中。根本不會退出來,也就是說Java中爲我們提供的中斷方法interrupt()並不能直接停止線程的執行。
查閱api,是這麼說的,其實interrupt()方法僅僅是爲我們設置了線程的中斷標誌,那麼我們是否可以按照這個思路對線程進行“真正意義上的”中斷呢?答案是可以的,Java還爲我們提供了interrupted()方法檢查中斷標誌。將上訴代碼稍作修改,我們再看看結果,可以發現的確可以正常停止了。

<span style="font-size:14px;">private static class NonBlockedTest implements Runnable {
		@Override
		public void run() {
			while (!Thread.interrupted()) {
				System.out.println("線程執行中...");
			}
		}
	}</span>

三、第二個中斷示例(阻塞線程)

<span style="font-size:14px;">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()");
		}
	}
 
}</span>

在這裏插入圖片描述
寫代碼的時候我們發現,我們沒辦法避免一個問題,就是當我們調用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()。

五、一個較爲複雜的中斷示例

<span style="font-size:14px;">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);
	}
}</span>

這個程序傳入一個參數代表主線程的睡眠時間。通過不同的睡眠時間,我們可以看到不同的結果。
當參數爲900(0.9s)的時候,也就是主線程會比子線程先被喚醒,這時候主線程調用子線程的interrupt()方法的時候,子線程還處於阻塞狀態,那麼程序會拋出中斷異常,並且重置中斷狀態,對應於中斷髮生在while語句和point1之間,這時候程序需要回收n1對象;

若是我把參數調到了1050(不同機器會有差別),也時候主線程發出中斷信號的時候。子線程剛好處於那個循環的耗時操作中,我們可以發現,子線程不會立即終止,而是繼續執行完for循環,就像前面說的,interrupt()不會中斷線程,這是需要自行檢查並執行的。那麼這種情況下,對應於代碼中point1和point2之間,程序會依次回收n2和n1對象,並且在下一次while檢查的時候檢測出中斷標誌並且退出,重置中斷標誌。通過這個例子,我們瞭解到了在中斷的時候正確處理的一些技巧和資源回收的必要性。

六、一句總結性的概括

若是我們調用線程的中斷方法,當程序即將進入或是已經進入阻塞調用的時候,那麼這個中斷信號應該由InterruptedException捕獲並進行重置;當run()方法程序段中不會出現阻塞操作的時候,這時候中斷並不會拋出異常,我們需要通過interrupted()方法進行中斷檢查和中斷標誌的重置。另外,知道IO操作和synchronized上的阻塞不可中斷也是必要的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章