Oracle官方併發教程之執行緒物件
在Java中,每個執行緒都是Thread類的例項。併發應用中一般有兩種不同的執行緒建立策略。
- 直接控制執行緒的建立和管理,每當應用程式需要執行一個非同步任務的時候就為其建立一個執行緒
- 將執行緒的管理從應用程式中抽象出來作為執行器,應用程式將任務傳遞給執行器,有執行器負責執行。
這一節,我們將討論Thread物件,有關Executors將在高階併發物件一節中討論。
定義並啟動一個執行緒
應用程式在建立一個執行緒例項時,必須提供需要線上程中執行的程式碼。有兩種方式去做到這一點:
- 提供一個Runnable物件。Runnable物件僅包含一個run()方法,在這個方法中定義的程式碼將在會執行緒中執行。將Runnable物件傳遞給Thread類的建構函式即可,如下面這個
public class HelloRunnable implements Runnable { public void run() { System.out.println("Hello from a thread!"); } public static void main(String args[]) { (new Thread(new HelloRunnable())).start(); } }
- 繼承Thread類。Thread類自身已實現了Runnable介面,但它的run()方法中並沒有定義任何程式碼。應用程式可以繼承與Thread類,並複寫run()方法。如例子
public class HelloThread extends Thread { public void run() { System.out.println("Hello from a thread!"); } public static void main(String args[]) { (new HelloThread()).start(); } }
需要注意的是,上述兩個例子都需要呼叫Thread.start()方法來啟動一個新的執行緒。 哪一種方式是我們應該使用的?相對來說,第一種更加通用,因為Runnable物件可以繼承於其他類(Java只支援單繼承,當一個類繼承與Thread類後,就無法繼承與其他類)。第二種方法更易於在簡單的應用程式中使用,但它的侷限就是:你的任務類必須是Thread的子類。這個課程更加聚焦於第一種將Runnable任務和Thread類分離的方式。不僅僅是因為這種方式更加靈活,更因為它更適合後面將要介紹的高階執行緒管理API。 Thread類定義了一些對執行緒管理十分有用的的方法。在這些方法中,有一些靜態方法可以給當前執行緒呼叫,它們可以提供一些有關執行緒的資訊,或者影響執行緒的狀態。而其他一些方法可以由其他執行緒進行呼叫,用於管理執行緒和Thread物件。我們將在下面的章節中,深入探討這些內容。
使用Sleep方法暫停一個執行緒
使用Thread.sleep()方法可以暫停當前執行緒一段時間。這是一種使處理器時間可以被其他執行緒或者運用程式使用的有效方式。sleep()方法還可以用於調整執行緒執行節奏(見下面的例子)和等待其他有執行時間需求的執行緒(這個例子將在下一節演示)。
在Thread中有兩個不同的sleep()方法,一個使用毫秒錶示休眠的時間,而另一個是用納秒。由於作業系統的限制休眠時間並不能保證十分精確。休眠週期可以被interrups所終止,我們將在後面看到這樣的例子。不管在任何情況下,我們都不應該假定呼叫了sleep()方法就可以將一個執行緒暫停一個十分精確的時間週期。
SleepMessages程式為我們展示了使用sleep()方法每四秒列印一個資訊的例子
public class SleepMessages { public static void main(String args[]) throws InterruptedException { String importantInfo[] = { "Mares eat oats", "Does eat oats", "Little lambs eat ivy", "A kid will eat ivy too" }; for (int i = 0; i < importantInfo.length; i++) { //Pause for 4 seconds Thread.sleep(4000); //Print a message System.out.println(importantInfo[i]); } } }
main()方法聲明瞭它有可能丟擲InterruptedException。當其他執行緒中斷當前執行緒時,sleep()方法就會丟擲該異常。由於這個應用程式並沒有定義其他的執行緒,所以並不用關心如何處理該異常。
中斷(Interrupts)
原文連結,譯文連結,譯者: 蘑菇街-小寶 校對:樑海艦
中斷是給執行緒的一個指示,告訴它應該停止正在做的事並去做其他事情。一個執行緒究竟要怎麼響應中斷請求取決於程式設計師,不過讓其終止是很普遍的做法。這是本文重點強調的用法。
一個執行緒通過呼叫對被中斷執行緒的Thread物件的interrupt()方法,傳送中斷訊號。為了讓中斷機制正常工作,被中斷的執行緒必須支援它自己的中斷(即要自己處理中斷)
中斷支援
執行緒如何支援自身的中斷?這取決於它當前正在做什麼。如果執行緒正在頻繁呼叫會拋InterruptedException異常的方法,在捕獲異常之後,它只是從run()方法中返回。例如,假設在SleepMessages的例子中,關鍵的訊息迴圈線上程的Runnable物件的run方法中,程式碼可能會被修改成下面這樣以支援中斷:
for (int i = 0; i < importantInfo.length; i++) { // Pause for 4 seconds try { Thread.sleep(4000); } catch (InterruptedException e) { // We've been interrupted: no more messages. return; } // Print a message System.out.println(importantInfo[i]); }
許多會拋InterruptedException異常的方法(如sleep()),被設計成接收到中斷後取消它們當前的操作,並在立即返回。
如果一個執行緒長時間執行而不呼叫會拋InterruptedException異常的方法會怎樣? 那它必須週期性地呼叫Thread.interrupted()方法,該方法在接收到中斷請求後返回true。例如:
for (int i = 0; i < inputs.length; i++) { heavyCrunch(inputs[i]); if (Thread.interrupted()) { // We've been interrupted: no more crunching. return; } }
在這個簡單的例子中,程式碼只是檢測中斷,並在收到中斷後退出執行緒。在更復雜的應用中,丟擲一個InterruptedException異常可能更有意義。
if (Thread.interrupted()){ throw new InterruptedException(); }
這使得中斷處理程式碼能集中在catch語句中。
中斷狀態標記
中斷機制通過使用稱為中斷狀態的內部標記來實現。呼叫Thread.interrupt()設定這個標記。當執行緒通過呼叫靜態方法Thread.interrupted()檢測中斷時,中斷狀態會被清除。非靜態的isInterrupted()方法被執行緒用來檢測其他執行緒的中斷狀態,不改變中斷狀態標記。
按照慣例,任何通過丟擲一個InterruptedException異常退出的方法,當拋該異常時會清除中斷狀態。不過,通過其他的執行緒呼叫interrupt()方法,中斷狀態總是有可能會立即被重新設定。
Joins
Join()方法可以讓一個執行緒等待另一個執行緒執行完成。若t是一個正在執行的Thread物件,
t.join();
將會使當前執行緒暫停執行並等待t執行完成。過載的join()方法可以讓開發者自定義等待週期。然而,和sleep()方法一樣join()方法依賴於作業系統的時間處理機制,你不能假定join()方法將會精確的等待你所定義的時長。
如同sleep()方法,join()方法響應中斷並在中斷時丟擲InterruptedException。
一個簡單的執行緒例子
下面這個簡單的例子將會把這一節的一些概念放到一起演示。SimpleThreads程式有兩個執行緒組成,第一個是主執行緒,它從建立了一個執行緒並等待它執行完成。如果MessageLoop執行緒執行了太長時間,主執行緒將會將其中斷。
MessageLoop現場將會列印一系列的資訊。如果中斷在它列印完所有資訊前發生,它將會列印一個特定的訊息並退出。
public class SimpleThreads { // Display a message, preceded by // the name of the current thread static void threadMessage(String message) { String threadName = Thread.currentThread().getName(); System.out.format("%s: %s%n", threadName, message); } private static class MessageLoop implements Runnable { public void run() { String importantInfo[] = { "Mares eat oats", "Does eat oats", "Little lambs eat ivy", "A kid will eat ivy too" }; try { for (int i = 0; i < importantInfo.length; i++) { // Pause for 4 seconds Thread.sleep(4000); // Print a message threadMessage(importantInfo[i]); } } catch (InterruptedException e) { threadMessage("I wasn't done!"); } } } public static void main(String args[]) throws InterruptedException { // Delay, in milliseconds before // we interrupt MessageLoop // thread (default one hour). long patience = 1000 * 60 * 60; // If command line argument // present, gives patience // in seconds. if (args.length > 0) { try { patience = Long.parseLong(args[0]) * 1000; } catch (NumberFormatException e) { System.err.println("Argument must be an integer."); System.exit(1); } } threadMessage("Starting MessageLoop thread"); long startTime = System.currentTimeMillis(); Thread t = new Thread(new MessageLoop()); t.start(); threadMessage("Waiting for MessageLoop thread to finish"); // loop until MessageLoop // thread exits while (t.isAlive()) { threadMessage("Still waiting..."); // Wait maximum of 1 second // for MessageLoop thread // to finish. t.join(1000); if (((System.currentTimeMillis() - startTime) > patience) && t.isAlive()) { threadMessage("Tired of waiting!"); t.interrupt(); // Shouldn't be long now // -- wait indefinitely t.join(); } } threadMessage("Finally!"); } }