跳到內容

執行緒和可執行的

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

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


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

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

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

執行緒

[編輯 | 編輯原始碼]

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

建立可執行的程序塊

[編輯 | 編輯原始碼]

一個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引數,它將用作類的名稱。然後,我們用0999之間的隨機數初始化類成員變數time。為了確保隨機數的初始化,我們在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程序塊僅僅是開始。沒有程式碼實際執行。為此,我們需要建立執行緒,然後這些執行緒分別執行這個任務。

建立執行緒

[編輯 | 編輯原始碼]

一旦我們有了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 程序物件的引用即可。在上面的例子中,我們將 RunnableProcess 類(我們在 程式碼清單 1 中建立)的類物件傳遞給 Thread 建構函式。但對於每個物件,我們賦予了不同的名稱(例如,"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

操作執行緒

[edit | edit source]

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

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

同步

[edit | edit source]

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

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.

執行緒在哪裡使用?

[edit | edit source]
影片遊戲大量使用執行緒

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

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

執行緒不僅在影片遊戲中被大量使用,而且在從簡單的瀏覽器應用程式到複雜的作業系統和網路應用程式的方方面面都很常見。今天,它往往超越了開發人員的簡單偏好,而是需要最大程度地利用基於大量多工處理的當代硬體。

參考

[edit | edit source]
  1. 單個 Java 應用程式可以同時執行的任務數量取決於作業系統允許多少任務進行多執行緒。

守護執行緒教程


Clipboard

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


華夏公益教科書