1. 程式人生 > >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())這樣的檢查(這樣會陷入死迴圈),原因下面會有解釋。

四、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上的阻塞不可中斷也是必要的。