跳轉到內容

執行緒和可執行物件

75% developed
來自華夏公益教科書

導航 併發程式設計 主題: v  d  e )


任何計算機的 CPU 都設計為在任何給定時間執行一項任務,但我們同時執行多個應用程式,一切都完美地協調一致。這不僅僅是因為 CPU 在執行計算方面非常快,而是因為 CPU 使用了一種巧妙的機制,將時間分配給各種任務。在計算機上呼叫的每個應用程式或任務都與 CPU 關聯,形成一個程序。因此,CPU 管理著各種程序,並在每個程序之間來回跳轉,給它一小部分時間和處理能力。這發生得如此之快,以至於對普通計算機使用者來說,感覺多個程序在同時執行。CPU 將時間分配給多個程序的能力稱為多工處理

因此,如果我們在計算機上執行一個 Java 應用程式,我們實際上是在與 CPU 建立一個程序,該程序獲得一小部分 CPU 時間。在 Java 的術語中,這個主程序被稱為守護程序守護執行緒。但是,Java 更進一步。它允許程式設計師將這個守護執行緒劃分為多個執行緒,這些執行緒同時執行(類似於 CPU),因此為 Java 應用程式提供了更精細的多工處理能力,稱為多執行緒

在本節中,我們將瞭解執行緒是什麼以及如何在 Java 程式中實現多執行緒,使其看起來協調一致且響應速度快。

執行緒

[edit | edit source]

鑑於上述討論,執行緒是作業系統可以排程的最小的處理單元。因此,使用執行緒,程式設計師可以有效地建立兩個或多個任務[1],這些任務同時執行。第一個行動呼叫是實現一組任務,這些任務由特定執行緒執行。為此,我們需要建立一個Runnable 程序。

建立可執行的程序塊

[edit | edit source]

Runnable 程序塊是一個簡單的類,它實現了一個run() 方法。在run() 方法中是需要由執行的執行緒執行的實際任務。透過實現使用 Runnable 介面的類,我們確保該類包含一個run() 方法。考慮以下程式

Computer code 程式碼清單 1: 可執行的程序
import java.util.Random;
public class RunnableProcess implements Runnable {
    private String name;
    private int time;
    private Random rand = new Random();

    public RunnableProcess(String name) {
        this.name = name;
        this.time = rand.nextInt(999);
    }

    public void run() {
        try {
            System.out.printf("%s is sleeping for %d \n", this.name, this.time);
            Thread.sleep(this.time);
            System.out.printf("%s is done.\n", this.name);
        } catch(Exception ex) {
            ex.printStackTrace();
        }
    }
}

在上面的程式碼中,我們建立了一個名為RunnableProcess 的類,並實現了Runnable 介面,以確保我們在類宣告中有一個run() 方法。

Example 程式碼節 1.1: 實現Runnable 介面
public class RunnableProcess implements Runnable {
    ...
    public void run() {
        ...
    }
}

然後我們宣告該類的其餘邏輯。對於建構函式,我們使用一個String 引數,該引數用作類的名稱。然後,我們將類成員變數time 初始化為0999 之間的隨機數。為了確保隨機數的初始化,我們在java.util 包中使用Random 類。

Example 程式碼節 1.2: 包含在0999 之間生成隨機整數的能力
import java.util.Random;
...
private Random rand = new Random();
...
this.time = rand.nextInt(999);

由該可執行塊執行的實際任務在run() 方法中呈現。為了防止由於併發程式設計而發生的異常,我們將此方法中的程式碼用try..catch 塊括起來。執行的任務實際上只包含三個語句。第一個輸出可執行程序的提供的名稱,最後一個報告執行緒已執行。也許程式碼中最有趣的的部分是第二個語句:Thread.sleep(...)

Example 程式碼節 1.3: 實際的可執行程序任務
...
System.out.printf("%s is sleeping for %d \n", this.name, this.time);
Thread.sleep(this.time);
System.out.printf("%s is done \n", this.name);
...

此語句允許執行當前可執行塊的執行緒停止其執行,持續給定時間。此時間以毫秒為單位。但為了我們的方便,此時間將是建構函式中生成的隨機數,可以在0999 毫秒之間。我們將在後面的部分探討這一點。建立Runnable 程序塊僅僅是開始。實際上沒有程式碼執行。為此,我們需要建立執行緒,然後這些執行緒將分別執行此任務。

建立執行緒

[edit | edit source]

一旦我們有了Runnable 程序塊,我們就可以建立各種執行緒,這些執行緒可以執行這些塊中包含的邏輯。Java 中的多執行緒能力是使用 Thread 類利用和操縱的。因此,Thread 物件包含建立真正的多執行緒程式所需的所有邏輯和裝置。考慮以下程式

Computer code 程式碼清單 2: 建立Thread 物件
public class ThreadLogic {
    public static void main(String[] args) {
        Thread t1 = new Thread(new RunnableProcess("Thread-1"));
        Thread t2 = new Thread(new RunnableProcess("Thread-2"));
        Thread t3 = new Thread(new RunnableProcess("Thread-3"));
    }
}

建立執行緒與上面程式所示一樣簡單。您只需建立一個Thread 類的物件,並將對Runnable 程序物件的引用傳遞給它。在上面的例子中,我們將Thread 建構函式與我們在 程式碼清單 1 中建立的RunnableProcess 類的類物件一起使用。但對於每個物件,我們都會給出不同的名稱(即,"Thread-1""Thread-2" 等等),以區分三個Thread 物件。上面的示例只聲明瞭Thread 物件,還沒有啟動它們執行。

啟動執行緒

[edit | edit source]

現在,我們知道了如何有效地建立一個Runnable 程序塊和一個執行它的Thread 物件,我們需要了解如何啟動建立的Thread 物件。這再簡單不過了。對於此過程,我們將呼叫Thread 物件上的start() 方法,瞧,我們的執行緒將開始執行它們各自的程序任務。

Computer code 程式碼清單 3: 啟動Thread 物件
public class ThreadLogic {
    public static void main(String[] args) {
        Thread t1 = new Thread(new RunnableProcess("Thread-1"));
        Thread t2 = new Thread(new RunnableProcess("Thread-2"));
        Thread t3 = new Thread(new RunnableProcess("Thread-3"));

        t1.start();
        t2.start();
        t3.start();
    }
}

以上程式碼將啟動所有三個宣告的執行緒。這樣,所有三個執行緒將逐個開始執行。但是,由於這是併發程式設計,並且我們為執行停止聲明瞭隨機時間,因此我們每個人的輸出都會有所不同。以下是我們在執行上述程式時收到的輸出。

Computer code 程式碼清單 3 的輸出
Thread-1 is sleeping for 419
Thread-3 is sleeping for 876
Thread-2 is sleeping for 189
Thread-2 is done
Thread-1 is done
Thread-3 is done

需要注意的是,Thread 的執行並沒有按照預期順序進行。執行緒不是按照 t1t2t3 的順序執行的,而是按照 t1t3t2 的順序執行的。執行緒執行的順序完全取決於作業系統,並且在每次執行程式時都可能發生變化,因此多執行緒應用程式的輸出難以預測和控制。有些人認為這是導致多執行緒程式設計及其除錯複雜化的主要原因。但是,應該觀察到,一旦使用 Thread.sleep(...) 函式將執行緒置於休眠狀態,就可以相當可靠地預測執行間隔和順序。睡眠時間最少的執行緒是 t2 ("Thread-2"),睡眠時間為 189 毫秒,因此它首先被呼叫。然後呼叫 t1,最後呼叫 t3

操作執行緒

[編輯 | 編輯原始碼]

可以說,執行緒的執行順序在一定程度上是透過使用 Thread.sleep(...) 方法來控制的。Thread 類具有這樣的靜態方法,這些方法可以說會影響執行緒的執行順序和操作。以下是 Thread 類中一些有用的靜態方法。這些方法在被呼叫時隻影響當前正在執行的執行緒。

方法 描述
Thread.currentThread() 返回任何給定時間正在執行的執行緒。
Thread.dumpStack() 列印當前正在執行執行緒的堆疊跟蹤。
Thread.sleep(long millis) 停止當前正在執行的執行緒執行給定時間(以毫秒為單位)。
丟擲 InterruptedException
Thread.sleep(long millis, int nanos) 停止當前正在執行的執行緒執行給定時間(以毫秒加上提供的納秒為單位)。
丟擲 InterruptedException
Thread.yield() 暫時暫停當前正在執行的執行緒的執行,以允許其他執行緒執行。

下面是一個建立和執行多個執行緒的示例,這些執行緒以同步的方式執行,以便當一個執行緒使用特定資源時,其他執行緒會等待直到該資源被釋放。我們將在後面的部分詳細介紹這一點。

Computer code 程式碼清單 4:建立以同步方式執行的多個 Thread 物件
public class MultiThreadExample {
    public static boolean cthread;
    public static String stuff = " printing material";

    public static void main(String args[]) {
        Thread t1 = new Thread(new RunnableProcess());
        Thread t2 = new Thread(new RunnableProcess());
        t1.setName("Thread-1");
        t2.setName("Thread-2");
        t2.start();
        t1.start();
    }
    /*
     * Prints information about the current thread and the index it is
     * on within the RunnableProcess
     */
    public static void printFor(int index) {
        StringBuffer sb = new StringBuffer();
        sb.append(Thread.currentThread().getName()).append(stuff);
        sb.append(" for the ").append(index).append(" time.");
        System.out.print(sb.toString());
    }
}
class RunnableProcess implements Runnable {
    public void run() {
        for(int i = 0; i < 10; i++) {
            synchronized(MultiThreadExample.stuff) {
                MultiThreadExample.printFor(i);
                try {
               	    MultiThreadExample.stuff.notifyAll();
                    MultiThreadExample.stuff.wait();
                } catch(InterruptedException ex) {
                   ex.printStackTrace();
                }
            }
        }
    }
}
Computer code 程式碼清單 4 的輸出
Thread-1 printing material for the 0 time.
Thread-2 printing material for the 0 time.
Thread-1 printing material for the 1 time.
Thread-2 printing material for the 1 time.
Thread-1 printing material for the 2 time.
Thread-2 printing material for the 2 time.
Thread-1 printing material for the 3 time.
Thread-2 printing material for the 3 time.
Thread-1 printing material for the 4 time.
Thread-2 printing material for the 4 time.
Thread-1 printing material for the 5 time.
Thread-2 printing material for the 5 time.
Thread-1 printing material for the 6 time.
Thread-2 printing material for the 6 time.
Thread-1 printing material for the 7 time.
Thread-2 printing material for the 7 time.
Thread-1 printing material for the 8 time.
Thread-2 printing material for the 8 time.
Thread-1 printing material for the 9 time.
Thread-2 printing material for the 9 time.

執行緒在哪些地方使用?

[編輯 | 編輯原始碼]
影片遊戲大量使用執行緒

執行緒在需要大量 CPU 使用量的應用程式中被大量使用。對於耗時且密集的操作,通常建議使用執行緒。此類應用程式的一個例子是典型的影片遊戲。在任何給定的時間,影片遊戲都涉及各種角色、周圍環境中的物體以及其他需要同時處理的細微差別。處理遊戲中的每個元素或物體都需要大量的執行緒來監控每個物體。

例如,以右側的角色扮演策略遊戲的螢幕截圖為例。這裡,遊戲畫面描繪了螢幕上移動的各種遊戲角色。現在想象一下處理螢幕上每個可見角色的移動、方向和行為。如果要一個接一個地完成這項任務,那麼移動每個角色肯定會花費很多時間。但是,如果使用多執行緒的基本原理,每個角色將以與其他角色同步的方式移動。

執行緒不僅在影片遊戲中被大量使用,而且在從簡單的瀏覽器應用程式到複雜的 operating systems 和網路應用程式的各個方面都普遍使用。如今,它往往超越了開發人員的簡單偏好,而是成為了最大限度地利用依賴於繁重的多工處理的現代硬體的必要性。

參考資料

[編輯 | 編輯原始碼]
  1. 單個 Java 應用程式可以同時執行的任務數量取決於作業系統允許多執行緒執行的任務數量。

守護執行緒教程


Clipboard

待辦事項
新增一些類似於 變數 中的練習


華夏公益教科書