1. 程式人生 > >多線程-interrupt(),isInterrupted(),interrupted()

多線程-interrupt(),isInterrupted(),interrupted()

中斷 串接 導致 row 以及 沒有 else 二次 清除

背景

  • 由於使用stop方法停止線程非常的暴力,可能會導致一系列問題,因此,提出一種溫和的方式:請求另外一個先不要在執行了,這就是中斷方式。
  • 此外有這樣的場景:編寫 一個程序,需要暫停一段時間,於是調用Thread.sleep(),但是編譯器或IDE報錯說沒有處理檢查到的InterruptedException。對於InterruptedException,一種常見的處理方式是“生吞”它:捕獲它,然後什麽也不做。
  • 在某個子線程中為了等待一些特定條件的到來, 你調用了Thread.sleep(10000), 預期線程睡10秒之後自己醒來, 但是如果這個特定條件提前到來的話, 來通知一個處於Sleep的線程。又比如說.線程通過調用子線程的join方法阻塞自己以等待子線程結束, 但是子線程運行過程中發現自己沒辦法在短時間內結束, 於是它需要想辦法告訴主線程別等我了. 這些情況下, 就需要中斷。
  • 在 java中啟動線程非常容易,大多數情況下我是讓一個線程執行完自己的任務然後自己停掉,但是有時候我們需要取消某個操作,比如你在網絡下載時,有時候需 要取消下載。實現線程的安全中斷並不是一件容易的事情,因為Java並不支持安全快速中斷線程的機制。

中斷

每個線程都有一個與之相關聯的 Boolean 屬性,用於表示線程的中斷狀態(interrupted status)中斷狀態初始時為 false;當另一個線程通過調用Thread.interrupt()中斷一個線程時,會出現以下兩種情況之一。(1)如果那個線程在執行一個低級可中斷阻塞方法,例如Thread.sleep(), Thread.join()或 Object.wait(),那麽它將取消阻塞並拋出InterruptedException。(2)否則,interrupt() 只是設置線程的中斷狀態。在被中斷線程中運行的代碼以後可以輪詢中斷狀態,看看它是否被請求停止正在做的事情。中斷狀態可以通過 Thread.isInterrupted()來讀取,並且可以通過一個名為Thread.interrupted() 的操作讀取和清除。

中斷是一種協作機制。當一個線程中斷另一個線程時,被中斷的線程不一定要立即停止正在做的事情。相反,中斷是禮貌地請求另一個線程在它願意並且方便的時候停止它正在做的事情。有些方法,例如Thread.sleep(),很認真地對待這樣的請求,但每個方法不是一定要對中斷作出響應。對於中斷請求,不阻塞但是仍然要花較長時間執行的方法可以輪詢中斷狀態,並在被中斷的時候提前返回。 您可以隨意忽略中斷請求,但是這樣做的話會影響響應。

中斷的協作特性所帶來的一個好處是,它為安全地構造可取消活動提供更大的靈活性。我們很少希望一個活動立即停止;如果活動在正在進行更新的時候被取消,那麽程序數據結構可能處於不一致狀態。中斷允許一個可取消活動來清理正在進行的工作,恢復不變量,通知其他活動它要被取消,然後才終止。

相關方法

this.interrupt()
中斷調用該方法的線程,除非當前線程正在中斷自己,否則checkAccess方法有可能拋出SecurityException異常。
(1)如果當前線程由於調用Object的wait(),或者Thread的join和sleep方法阻塞,則中斷狀態將被清除,並且拋出InterrruptedException異常。

(2)如果線程由於InterruptibleChannel的IO操作阻塞,則通道將關閉,線程設置中斷狀態,並且線程收到一個ClosedInterruptException異常。

(3)如果線程由於Selector阻塞,線程將處於中斷狀態,並且從selection操作立刻返回,可能是一個非零值。
除此之外,線程將被設置中斷狀態。

總的來說:interrupt方法有兩個作用:(1)強線程的中斷狀態設置為true(2)讓被阻塞的線程拋出InterruptedException異常(同時中斷狀態為false)。

這是很重要的。這樣,對於那些阻塞方法(比如 wait() 和 sleep())而言,當另一個線程調用interrupt()中斷該線程時,該線程會從阻塞狀態退出並且拋出中斷異常。這樣,我們就可以捕捉到中斷異常,並根據實際情況對該線程從阻塞方法中異常退出而進行一些處理。

註意:沒有占用CPU運行的線程是不可能給自己的中斷狀態置位的,就會產生一個InterruptedException異常。

比如說:線程A獲得了鎖進入了同步代碼塊中,但由於條件不足調用 wait() 方法阻塞了。這個時候,線程B執行 threadA.interrupt()請求中斷線程A,此時線程A就會拋出InterruptedException,我們就可以在catch中捕獲到這個異常並進行相應處理(比如進一步往上拋出)

this.isInterrupted()
檢測調用該方法的線程是否被中斷,中斷狀態不會被清除。線程一旦被中斷,該方法返回true,而一旦sleep等方法拋出異常,它將清除中斷狀態,此時方法將返回false。

package com.huawei.thread;

public class Test30 {

	public static void main(String[] args) {
		Thread t = Thread.currentThread();
		System.out.println("1: " + t.isInterrupted());
		t.interrupt();
		System.out.println("2: " + t.isInterrupted());
		System.out.println("3: " + t.isInterrupted());
		try {
			Thread.sleep(2000);
			System.out.println("not interrted...");
		} catch (Exception e) {
			System.out.println("interrupted...");
			System.out.println("4: " + t.isInterrupted());
		}
		System.out.println("5: " + t.isInterrupted());
	}

}

技術分享

Thread.interrupted()
檢測當前線程是否被中斷,並且中斷狀態會被清除(即重置為false);由於它是靜態方法,因此不能再特定的線程上使用,只能報告調用它的線程的中斷狀態;如果該方法被調用兩次,則第二次一般是返回false,如果線程不存活,則返回false。

package com.huawei.thread;

public class Test31 {
	public static void main(String[] args) {
		System.out.println("1: " + Thread.interrupted());
		Thread.currentThread().interrupt();
		System.out.println("2: " + Thread.interrupted());
		System.out.println("3: " + Thread.interrupted());
	}
}

 技術分享

阻塞方法

當一個方法拋出InterruptedException時,它不僅告訴你它可以拋出一個特定的檢查異常,而且還告訴你其他一些事情。例如,它告訴你它是一個阻塞blocking方法,如果你響應得當的話,它將嘗試消除阻塞並盡早返回。

阻塞方法不同於一般的要運行較長時間的方法。一般方法的完成只取決於它所要做的事情,以及是否有足夠多可用的計算資源(CPU 周期和內存)。而阻塞方法的完成還取決於一些外部的事件,例如計時器到期,I/O 完成,或者另一個線程的動作(釋放一個鎖,設置一個標誌,或者將一個任務放在一個工作隊列中)。一般方法在它們的工作做完後即可結束,而阻塞方法較難於預測,因為它們取決於外部事件。阻塞方法可能影響響應能力,因為難於預測它們何時會結束。

阻塞方法可能因為等不到所等的事件而無法終止,因此令阻塞方法可取消就非常有用(如果長時間運行的非阻塞方法是可取消的,那麽通常也非常有用)。可取消操作是指能從外部使之在正常完成之前終止的操作。由Thread提供並受Thread.sleep()和Object.wait()支持的中斷機制就是一種取消機制;它允許一個線程請求另一個線程停止它正在做的事情。當一個方法拋出InterruptedException時,它是在告訴你,如果執行該方法的線程被中斷,它將嘗試停止它正在做的事情而提前返回,並通過拋出InterruptedException表明它提前返回。行為良好的阻塞庫方法應該能對中斷作出響應並拋出InterruptedException,以便能夠用於可取消活動中,而不至於影響響應。

不可中斷的阻塞方法

並非所有的阻塞方法都拋出InterruptedException。輸入和輸出流類會阻塞等待 I/O 完成,但是它們不拋出InterruptedException,而且在被中斷的情況下也不會提前返回。然而,對於套接字 I/O,如果一個線程關閉套接字,則那個套接字上的阻塞 I/O 操作將提前結束,並拋出一個SocketException。java.nio中的非阻塞 I/O 類也不支持可中斷 I/O,但是同樣可以通過關閉通道或者請求Selector上的喚醒來取消阻塞操作。類似地,嘗試獲取一個內部鎖的操作(進入一個synchronized 塊)是不能被中斷的,但是ReentrantLock支持可中斷的獲取模式。

處理不支持中斷的線程中斷的常用方法

如果一個線程由於同步記性IO操作導致阻塞,中斷請求不會拋出InterruptedException,該如何中斷此線程呢?

package com.huawei.thread;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;

public class Test32 extends Thread {
	public static final int BUF_SIZE = 512;

	Socket socket;
	InputStream in;

	public Test32(Socket socket) throws IOException {
		this.socket = socket;
		this.in = socket.getInputStream();
	}

	@Override
	public void interrupt() {
		try {
			socket.close();
		} catch (IOException e) {

		} finally {
			super.interrupt();
		}
	}

	@Override
	public void run() {
		try {
			byte[] buf = new byte[BUF_SIZE];
			while (true) {
				int count = in.read(buf);
				if (count < 0) {
					break;
				} else if (count > 0) {

				}
			}
		} catch (IOException e) {

		}

	}

}

通過改寫了Thread.interrupt方法,當調用interrupt方法時,會關閉socket,如果此時read方法阻塞,那麽會拋出IOException異常,此時線程任務也就結束了。

處理InterrruptedException

(1)如果拋出InterrruptedException意味著一個方法是阻塞方法,那麽調用一個阻塞方法則意味著你的方法也是一個阻塞方法,應該有某種策略來處理InterrruptedException。通常最容易的策略是自己拋出InterrruptedException,這樣做可以使方法對中斷做出響應,而且只需將InterruptedException添加到throws子句。

public class TaskQueue {
    private static final int MAX_TASKS = 1000;
 
    private BlockingQueue<Task> queue = new LinkedBlockingQueue<Task>(MAX_TASKS);
 
    public void putTask(Task r) throws InterruptedException { 
        queue.put(r);
    }
 
    public Task getTask() throws InterruptedException { 
        return queue.take();
    }
}

(2)有時候需要在傳播異常之前進行一些清理工作,在這種情況下,可以捕獲InterruptedException,執行清理,然後拋出異常。  

public class PlayerMatcher {
    private PlayerSource players;
 
    public PlayerMatcher(PlayerSource players) { 
        this.players = players; 
    }
 
    public void matchPlayers() throws InterruptedException { 
        try {
             Player playerOne, playerTwo;
             while (true) {
                 playerOne = playerTwo = null;
                 // Wait for two players to arrive and start a new game
                 playerOne = players.waitForPlayer(); // could throw IE
                 playerTwo = players.waitForPlayer(); // could throw IE
                 startNewGame(playerOne, playerTwo);
             }
         }
         catch (InterruptedException e) {  
             // If we got one player and were interrupted, put that player back
             if (playerOne != null)
                 players.addFirst(playerOne);
             // Then propagate the exception
             throw e;
         }
    }
}

(3)不要生吞中斷:有時候拋出InterruptedException並不合適,例如當由Runnable定義的任務調用一個可中斷的方法時,在這種情況下,不能重新拋出InterruptedException,因為Runnable接口的run方法不允許拋出異常。當一個阻塞方法檢測到中斷並拋出InterruptedException但是不能重新拋出它,那麽應該保留中斷發生的證據,以便調用棧中更高層的代碼能知道中斷,並對中斷做出響應,該任務可以通過調用interrupt()以重新中斷當前線程來完成。

public class TaskRunner implements Runnable {
    private BlockingQueue<Task> queue;
 
    public TaskRunner(BlockingQueue<Task> queue) { 
        this.queue = queue; 
    }
 
    public void run() { 
        try {
             while (true) {
                 Task task = queue.take(10, TimeUnit.SECONDS);
                 task.execute();
             }
         }
         catch (InterruptedException e) { 
             // Restore the interrupted status
             Thread.currentThread().interrupt();
         }
    }
}  

待決中斷

sleep等方法的實現檢查到阻塞線程被中斷,它會友好的終止線程,並拋出InterruptedException異常。另外一種情況,如果線程在調用sleep等方法之前被中斷,那麽該中斷就稱為:待決中斷,它會在剛調用sleep等方法時,立刻拋出InterruptedException異常。

package com.huawei.thread;

public class Test29 {

	public static void main(String[] args) {
		Thread.currentThread().interrupt();
		long start = System.currentTimeMillis();
		try {
			Thread.sleep(40000);
			System.out.println("not interrupted...");
		} catch (Exception e) {
			System.out.println("interrupted...");
		}
		System.out.println(System.currentTimeMillis() - start);
	}
}

運行結果如下:

技術分享

這種模式下,main線程中斷它自身。除了將中斷標誌(它是Thread的內部標誌)設置為true外,沒有其他任何影響。線程被中斷了,但main線程仍然運行,main線程繼續監視實時時鐘,並進入try塊,一旦調用sleep()方法,它就會註意到待決中斷的存在,並拋出InterruptException。於是執行跳轉到catch塊,並打印出線程被中斷的信息。最後,計算並打印出時間差。

實例1

package com.huawei.thread;

public class Test27 {

	public static void main(String[] args) {
		try {
			MyThread1 t = new MyThread1();
			t.start();
			Thread.sleep(2000);
			t.interrupt(); // 中斷線程t
		} catch (InterruptedException e) {
			System.out.println("main catch");
			e.printStackTrace();
		}
		System.out.println("end main");
	}

}

class MyThread1 extends Thread {
	@Override
	public void run() {
		for (int i = 1; i < 500000; i++) {
			if (Thread.interrupted()) {
				System.out.println("stop and exit...");
				break;
			}
			System.out.println("i: " + i);
		}
                // 盡管線程被中斷,但並沒有結束運行,這行代碼還是被執行。
		System.out.println("end run...");
	}
}

運行結果部分截圖:

技術分享

當t獲取CPU執行時,if判斷中,檢測到中斷狀態,即在main線程中調用的t.interrupt(),for循環執行break跳出for循環,但是線程並沒有結束,還是繼續執行for後面的語句。可以將break改成return,則線程立刻結束執行。

當然也可以拋出InterruptedException異常:

class MyThread1 extends Thread {
	@Override
	public void run() {
		try {
			for (int i = 1; i < 500000; i++) {
				if (Thread.interrupted()) {
					System.out.println("stop and exit...");
					throw new InterruptedException();
				}
				System.out.println("i: " + i);
			}
			System.out.println("end run...");
		} catch (Exception e) {
			System.out.println("catch interrupted...");
		}
		
	}
}  

當檢測到中斷標識為true,拋出異常,這樣該線程就不會再執行其他的正常語句。

前面說過不能生吞中斷,修改如下:

class MyThread1 extends Thread {
	@Override
	public void run() {
		try {
			for (int i = 1; i < 500000; i++) {
				if (Thread.interrupted()) {
					System.out.println("stop and exit...");
					throw new InterruptedException();
				}
				System.out.println("i: " + i);
			}
			System.out.println("end run...");
		} catch (Exception e) {
			System.out.println("catch interrupted...");
			Thread.currentThread().interrupt(); // 不生吞中斷 
		}
		
	}
}  

這樣,就由生吞異常,將異常進一步擴散了。

實例2

package com.huawei.thread;

public class Test28 extends Thread {
	volatile Boolean stop = false;

	public static void main(String[] args) throws InterruptedException {
		Test28 t = new Test28();
		System.out.println("starting thread...");
		t.start();

		Thread.sleep(3000);

		System.out.println("asking thread to stop...");
                // 必須要在interrupt之前設置
		// 如果線程阻塞,將不會檢查此變量,調用interrupt之後,線程就可以盡早的終結被阻塞狀態,能夠檢查這一變量
		t.stop = true;
                // 如果線程沒有被阻塞,這時調用interrupt將不起作用。
		// 這一方法實際上完成的是:在線程受到阻塞時拋出一個中斷信號,這樣線程就可以退出阻塞狀態
		t.interrupt();

		Thread.sleep(3000);
		System.out.println("stopping app...");
	}

	@Override
	public void run() {
		while (!stop) {
			System.out.println("running...");
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				System.out.println("interrupted...");
			}
		}
		System.out.println("thread exit...");
	}

}  

把握幾個重點:stop變量、run方法中的sleep()、interrupt()、InterruptedException。串接起來就是這個意思:

  • 當我們在run方法中調用sleep(或其他阻塞線程的方法)時,如果線程阻塞的時間過長,比如10s,那在這10s內,線程阻塞,run方法不被執行;
  • 但是如果在這10s內,stop被設置成true,表明要終止這個線程;但是,現在線程是阻塞的,它的run方法不能執行,自然也就不能檢查stop,所以線程不能終止;
  • 這個時候,我們就可以用interrupt()方法了:我們在thread.stop = true;語句後調用thread.interrupt()方法, 該方法將在線程阻塞時拋出一個中斷信號,該信號將被catch語句捕獲到,一旦捕獲到這個信號,線程就提前終結自己的阻塞狀態;
  • 這樣,它就能夠 再次運行run 方法了,然後檢查到stop = true,while循環就不會再被執行,在執行了while後面的清理工作之後,run方法執行完 畢,線程終止。

當代碼調用中需要拋出一個InterruptedException,可以選擇吧中斷狀態復位,也可以選在向外拋出InterruptedException,由外層的調用者來決定。

參考資料

https://www.ibm.com/developerworks/cn/java/j-jtp05236.html

http://www.cnblogs.com/hapjin/p/5450779.html

http://blog.csdn.net/canot/article/details/51087772

http://blog.csdn.net/ns_code/article/details/17091267

http://developer.51cto.com/art/201508/487231.htm

多線程-interrupt(),isInterrupted(),interrupted()