Java併發程式設計(二)多執行緒程式設計
在上一節,我們介紹了程序與執行緒的概念,接下來介紹如何使用多執行緒(暫不介紹多程序)。
2. Thread物件
每個執行緒都對應一個Thread例項,存在兩種策略使用Thread類來建立併發程式。
- 直接進行執行緒的建立和管理,也就是當需要開啟一個非同步任務時,例項化一個Thread物件。
- 抽象執行緒管理,將執行任務交給執行器。
本節介紹如何使用Thread類。
2.1 定義和執行新的執行緒
在建立新執行緒的時候,我們需要指定該執行緒執行的程式碼。我們有兩種途徑:
- 提供一個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類實現了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物件,是更加通用的,因為Runnable可以繼承一個不是Thread類的物件。第二種方式在簡單的程式中使用更加方便,但是其限制了執行的任務必須是Thread的子類。該教程側重與第一種風格,其將Runnable與Thread分離。該方法不僅更加靈活,也適用於高級別的執行緒管理APIs(在以後介紹)。
Thread類定義了一些執行緒管理的方法。這些包括一些提供關於執行該方法的執行緒的資訊,或者影響該執行緒的狀態的靜態(static)方法。其他方法為非靜態方法,在另一個執行緒中呼叫,用來維護Thread物件。
2.2 使用Sleep暫停執行緒
Thread.sleep導致當前執行緒暫停一個指定的時間。這是一個有效的方式,來使得其他程序或執行緒可以使用處理器。sleep方法可以用於調整程式碼執行的節奏,如接下來的程式碼,或者等待其他執行緒的完成,如本節最後的SimpleThreads例子。
Java提供了兩個過載的sleep方法,一個指定休眠的毫秒記時,一個是納秒記時。但是,該兩個方法都不能保證休眠時間的準確性,這是因為他們被底層的系統控制的。另外,休眠的時間段可以被中斷終止。在任何情況下,我們不能假設sleep可以暫停一個精確的時間。
SleepMessages示例瞭如何使用sleep以4s為間隔輸出一個訊息。
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 異常。當其他執行緒中斷一個處於休眠狀態下的執行緒時,會丟擲該異常。由於該程式並沒有定義其他可以導致中斷的執行緒,所以沒有處理該異常(catch),只是丟擲(throw)。
2.3 中斷
中斷是一個訊號,說明該執行緒應該停止當前的任務,去做其他的任務。由開發者決定一個執行緒如何響應中斷,但是終止該執行緒的常見的。
一個執行緒可以呼叫interrupt()方法來中斷另一個執行緒。為了中斷機制正常執行,中斷的執行緒必須支援自己的中斷操作。
2.3.1 支援中斷操作
一個執行緒如何支援中斷操作呢?這取決於該執行緒當前做什麼。如果該執行緒頻繁呼叫丟擲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,來判斷其是否被中斷,
例如,
for (int i = 0; i < inputs.length; i++) {
heavyCrunch(inputs[i]);
if (Thread.interrupted()) {
// We've been interrupted: no more crunching.
return;
}
}
在該例子中,程式碼僅僅測試了中斷和退出執行緒。在更復雜的程式中,丟擲一個異常會更加有意義,
if (Thread.interrupted()) {
throw new InterruptedException();
}
這允許在catch語句中加入中斷處理程式碼。
2.3.2 中斷標誌位
中斷機制通過一個成為中斷狀態(interrupt status)的標誌位來實現。呼叫Thread.interrupt將會設定該標誌。當一個執行緒呼叫靜態方法Thread.interrupted方法來檢查中斷時,中斷狀態被清除。非靜態方法isInterrupted,用來檢查另一個執行緒的中斷狀態,並不會改變執行緒狀態標誌。
按照規定,任何通過丟擲InterruptedException異常來退出的執行緒都會清除中斷狀態。當另一個執行緒呼叫interrupt後,該執行緒的中斷狀態會被重新設定。
2.4 聯合(Joins)
join方法允許一個執行緒等待另一個執行緒執行結束。如果t是一個正在執行的執行緒物件,
t.join();
會導致當前執行緒暫停執行直到執行緒t執行結束。joins方法的過載方法允許開發者指定等待的週期。然而,由於join和sleep依賴於系統的時間,所以你不能假設join會精確等待你指定的時間。
和sleep一樣,join會丟擲一個InterruptedException異常退出執行緒,來相應中斷操作。
2.5 SimpleThreads例子
下面的例子將一些概念包括在一起。SimpleThreads包括兩個執行緒。一個是主執行緒,每個java程式都會包括一個主執行緒。主執行緒從Runnable物件建立了MessageLoop執行緒,並且等待該執行緒執行完畢。如果MessageLoop執行緒執行時間過長,主執行緒中斷該執行緒。
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!");
}
}