天天看點

Java基礎知識---線程的中斷一、什麼是線程切換,線程阻塞,線程中斷?二、第一個中斷示例(非阻塞線程)三、第二個中斷示例(阻塞線程)四、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>
           
Java基礎知識---線程的中斷一、什麼是線程切換,線程阻塞,線程中斷?二、第一個中斷示例(非阻塞線程)三、第二個中斷示例(阻塞線程)四、Java中線程中斷的工作原理五、一個較為複雜的中斷示例六、一句總結性的概括

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

<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>
           
Java基礎知識---線程的中斷一、什麼是線程切換,線程阻塞,線程中斷?二、第一個中斷示例(非阻塞線程)三、第二個中斷示例(阻塞線程)四、Java中線程中斷的工作原理五、一個較為複雜的中斷示例六、一句總結性的概括

寫代碼的時候我們發現,我們沒辦法避免一個問題,就是當我們調用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上的阻塞不可中斷也是必要的。