在上一節,我們介紹了程序與執行緒的概念,接下來介紹如何使用多執行緒(暫不介紹多程序)。

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!");
    }
}