Java 程式中的多執行緒

azz發表於2007-08-24
由於在語言級提供了執行緒支援,在 Java 語言中使用多執行緒要遠比在 C 或 C++ 中來得簡單。本文通過簡單的程式示例展現了在 Java 程式中執行緒程式設計的簡單性。在學習完本文後,使用者應該能夠編寫簡單、多執行緒的程式。
<!--START RESERVED FOR FUTURE USE INCLUDE FILES--><!-- include java script once we verify teams wants to use this and it will work on dbcs and cyrillic characters --><!--END RESERVED FOR FUTURE USE INCLUDE FILES-->

在 Java 程式中使用多執行緒要比在 C 或 C++ 中容易得多,這是因為 Java 程式語言提供了語言級的支援。本文通過簡單的程式設計示例來說明 Java 程式中的多執行緒是多麼直觀。讀完本文以後,使用者應該能夠編寫簡單的多執行緒程式。

為什麼會排隊等待?

下面的這個簡單的 Java 程式完成四項不相關的任務。這樣的程式有單個控制執行緒,控制在這四個任務之間線性地移動。此外,因為所需的資源 ― 印表機、磁碟、資料庫和螢幕 -- 由於硬體和軟體的限制都有內在的潛伏時間,所以每項任務都包含明顯的等待時間。因此,程式在訪問資料庫之前必須等待印表機完成列印檔案的任務,等等。如果您正在等待程式的完成,則這是對計算資源和您的時間的一種拙劣使用。改進此程式的一種方法是使它成為多執行緒的。

四項不相關的任務

class myclass {
static public void main(String args[]) {
    print_a_file();
    manipulate_another_file();
    access_database();
    draw_picture_on_screen();
    }
}

在本例中,每項任務在開始之前必須等待前一項任務完成,即使所涉及的任務毫不相關也是這樣。但是,在現實生活中,我們經常使用多執行緒模型。我們在處理某些任務的同時也可以讓孩子、配偶和父母完成別的任務。例如,我在寫信的同時可能打發我的兒子去郵局買郵票。用軟體術語來說,這稱為多個控制(或執行)執行緒。

可以用兩種不同的方法來獲得多個控制執行緒:

  • 多個程式
    在大多數作業系統中都可以建立多個程式。當一個程式啟動時,它可以為即將開始的每項任務建立一個程式,並允許它們同時執行。當一個程式因等待網路訪問或使用者輸入而被阻塞時,另一個程式還可以執行,這樣就增加了資源利用率。但是,按照這種方式建立每個程式要付出一定的代價:設定一個程式要佔用相當一部分處理器時間和記憶體資源。而且,大多數作業系統不允許程式訪問其他程式的記憶體空間。因此,程式間的通訊很不方便,並且也不會將它自己提供給容易的程式設計模型。
  • 執行緒
    執行緒也稱為輕型程式 (LWP)。因為執行緒只能在單個程式的作用域內活動,所以建立執行緒比建立程式要廉價得多。這樣,因為執行緒允許協作和資料交換,並且在計算資源方面非常廉價,所以執行緒比程式更可取。執行緒需要作業系統的支援,因此不是所有的機器都提供執行緒。Java 程式語言,作為相當新的一種語言,已將執行緒支援與語言本身合為一體,這樣就對執行緒提供了強健的支援。

 

使用 Java 程式語言實現執行緒

Java 程式語言使多執行緒如此簡單有效,以致於某些程式設計師說它實際上是自然的。儘管在 Java 中使用執行緒比在其他語言中要容易得多,仍然有一些概念需要掌握。要記住的一件重要的事情是 main() 函式也是一個執行緒,並可用來做有用的工作。程式設計師只有在需要多個執行緒時才需要建立新的執行緒。

Thread 類

Thread 類是一個具體的類,即不是抽象類,該類封裝了執行緒的行為。要建立一個執行緒,程式設計師必須建立一個從 Thread 類匯出的新類。程式設計師必須覆蓋 Thread 的 run() 函式來完成有用的工作。使用者並不直接呼叫此函式;而是必須呼叫 Thread 的 start() 函式,該函式再呼叫 run()。下面的程式碼說明了它的用法:

建立兩個新執行緒

import java.util.*;
class TimePrinter extends Thread {
    int pauseTime;
    String name;
    public TimePrinter(int x, String n) {
        pauseTime = x;
        name = n;
    }
    public void run() {
        while(true) {
            try {
                System.out.println(name + ":" + new 
                    Date(System.currentTimeMillis()));
                Thread.sleep(pauseTime);
            } catch(Exception e) {
                System.out.println(e);
            }
        }
    }
    static public void main(String args[]) {
        TimePrinter tp1 = new TimePrinter(1000, "Fast Guy");
        tp1.start();
        TimePrinter tp2 = new TimePrinter(3000, "Slow Guy");
        tp2.start();
    
    }
}

在本例中,我們可以看到一個簡單的程式,它按兩個不同的時間間隔(1 秒和 3 秒)在螢幕上顯示當前時間。這是通過建立兩個新執行緒來完成的,包括 main() 共三個執行緒。但是,因為有時要作為執行緒執行的類可能已經是某個類層次的一部分,所以就不能再按這種機制建立執行緒。雖然在同一個類中可以實現任意數量的介面,但 Java 程式語言只允許一個類有一個父類。同時,某些程式設計師避免從 Thread 類匯出,因為它強加了類層次。對於這種情況,就要 runnable 介面

Runnable 介面

此介面只有一個函式,run(),此函式必須由實現了此介面的類實現。但是,就執行這個類而論,其語義與前一個示例稍有不同。我們可以用 runnable 介面改寫前一個示例。(不同的部分用黑體表示。)

建立兩個新執行緒而不強加類層次

import java.util.*;
class TimePrinter 
        implements Runnable {
    int pauseTime;
    String name;
    public TimePrinter(int x, String n) {
        pauseTime = x;
        name = n;
    }
    public void run() {
        while(true) {
            try {
                System.out.println(name + ":" + new 
                    Date(System.currentTimeMillis()));
                Thread.sleep(pauseTime);
            } catch(Exception e) {
                System.out.println(e);
            }
        }
    }
    static public void main(String args[]) {
        Thread t1 = new Thread (new TimePrinter(1000, "Fast Guy"));
        t1.start();
        Thread t2 = new Thread (new TimePrinter(3000, "Slow Guy"));
        t2.start();
    
    }
}
      

請注意,當使用 runnable 介面時,您不能直接建立所需類的物件並執行它;必須從 Thread 類的一個例項內部執行它。許多程式設計師更喜歡 runnable 介面,因為從 Thread 類繼承會強加類層次。

synchronized 關鍵字

到目前為止,我們看到的示例都只是以非常簡單的方式來利用執行緒。只有最小的資料流,而且不會出現兩個執行緒訪問同一個物件的情況。但是,在大多數有用的程式中,執行緒之間通常有資訊流。試考慮一個金融應用程式,它有一個 Account 物件,如下例中所示:

一個銀行中的多項活動

public class Account {
    String holderName;
    float amount;
    public Account(String name, float amt) {
        holderName = name;
        amount = amt;
    }
    public void deposit(float amt) {
        amount += amt;
    }
    public void withdraw(float amt) {
        amount -= amt;
    }
    public float checkBalance() {
        return amount;
    }
}

在此程式碼樣例中潛伏著一個錯誤。如果此類用於單執行緒應用程式,不會有任何問題。但是,在多執行緒應用程式的情況中,不同的執行緒就有可能同時訪問同一個 Account 物件,比如說一個聯合帳戶的所有者在不同的 ATM 上同時進行訪問。在這種情況下,存入和支出就可能以這樣的方式發生:一個事務被另一個事務覆蓋。這種情況將是災難性的。但是,Java 程式語言提供了一種簡單的機制來防止發生這種覆蓋。每個物件在執行時都有一個關聯的鎖。這個鎖可通過為方法新增關鍵字 synchronized 來獲得。這樣,修訂過的 Account 物件(如下所示)將不會遭受像資料損壞這樣的錯誤:

對一個銀行中的多項活動進行同步處理

public class Account {
    String holderName;
    float amount;
    public Account(String name, float amt) {
        holderName = name;
        amount = amt;
    }
    public 
        synchronized void deposit(float amt) {
        amount += amt;
    }
    public 
        synchronized void withdraw(float amt) {
        amount -= amt;
    }
    public float checkBalance() {
        return amount;
    }
}
      

deposit() 和 withdraw() 函式都需要這個鎖來進行操作,所以當一個函式執行時,另一個函式就被阻塞。請注意, checkBalance() 未作更改,它嚴格是一個讀函式。因為 checkBalance() 未作同步處理,所以任何其他方法都不會阻塞它,它也不會阻塞任何其他方法,不管那些方法是否進行了同步處理。

 

Java 程式語言中的高階多執行緒支援

執行緒組
執行緒是被個別建立的,但可以將它們歸類到 執行緒組中,以便於除錯和監視。只能在建立執行緒的同時將它與一個執行緒組相關聯。在使用大量執行緒的程式中,使用執行緒組組織執行緒可能很有幫助。可以將它們看作是計算機上的目錄和檔案結構。

執行緒間發信
當執行緒在繼續執行前需要等待一個條件時,僅有 synchronized 關鍵字是不夠的。雖然 synchronized 關鍵字阻止併發更新一個物件,但它沒有實現 執行緒間發信 。Object 類為此提供了三個函式:wait()、notify() 和 notifyAll()。以全球氣候預測程式為例。這些程式通過將地球分為許多單元,在每個迴圈中,每個單元的計算都是隔離進行的,直到這些值趨於穩定,然後相鄰單元之間就會交換一些資料。所以,從本質上講,在每個迴圈中各個執行緒都必須等待所有執行緒完成各自的任務以後才能進入下一個迴圈。這個模型稱為 遮蔽同步,下例說明了這個模型:

遮蔽同步

public class BSync {
    int totalThreads;
    int currentThreads;
    public BSync(int x) {
        totalThreads = x;
        currentThreads = 0;
    }
    public synchronized void waitForAll() {
        currentThreads++;
        if(currentThreads < totalThreads) {
            try {
                wait();
            } catch (Exception e) {}
        }
        else {
            currentThreads = 0;
            notifyAll();
        }
    }
}

當對一個執行緒呼叫 wait() 時,該執行緒就被有效阻塞,只到另一個執行緒對同一個物件呼叫 notify() 或 notifyAll() 為止。因此,在前一個示例中,不同的執行緒在完成它們的工作以後將呼叫 waitForAll() 函式,最後一個執行緒將觸發 notifyAll() 函式,該函式將釋放所有的執行緒。第三個函式 notify() 只通知一個正在等待的執行緒,當對每次只能由一個執行緒使用的資源進行訪問限制時,這個函式很有用。但是,不可能預知哪個執行緒會獲得這個通知,因為這取決於 Java 虛擬機器 (JVM) 排程演算法。

將 CPU 讓給另一個執行緒
當執行緒放棄某個稀有的資源(如資料庫連線或網路埠)時,它可能呼叫 yield() 函式臨時降低自己的優先順序,以便某個其他執行緒能夠執行。

守護執行緒
有兩類執行緒:使用者執行緒和守護執行緒。 使用者執行緒是那些完成有用工作的執行緒。 守護執行緒 是那些僅提供輔助功能的執行緒。Thread 類提供了 setDaemon() 函式。Java 程式將執行到所有使用者執行緒終止,然後它將破壞所有的守護執行緒。在 Java 虛擬機器 (JVM) 中,即使在 main 結束以後,如果另一個使用者執行緒仍在執行,則程式仍然可以繼續執行。





回頁首

避免不提倡使用的方法

不提倡使用的方法是為支援向後相容性而保留的那些方法,它們在以後的版本中可能出現,也可能不出現。Java 多執行緒支援在版本 1.1 和版本 1.2 中做了重大修訂,stop()、suspend() 和 resume() 函式已不提倡使用。這些函式在 JVM 中可能引入微妙的錯誤。雖然函式名可能聽起來很誘人,但請抵制誘惑不要使用它們。





回頁首

除錯執行緒化的程式

線上程化的程式中,可能發生的某些常見而討厭的情況是死鎖、活鎖、記憶體損壞和資源耗盡。

死鎖
死鎖可能是多執行緒程式最常見的問題。當一個執行緒需要一個資源而另一個執行緒持有該資源的鎖時,就會發生死鎖。這種情況通常很難檢測。但是,解決方案卻相當好:在所有的執行緒中按相同的次序獲取所有資源鎖。例如,如果有四個資源 ―A、B、C 和 D ― 並且一個執行緒可能要獲取四個資源中任何一個資源的鎖,則請確保在獲取對 B 的鎖之前首先獲取對 A 的鎖,依此類推。如果“執行緒 1”希望獲取對 B 和 C 的鎖,而“執行緒 2”獲取了 A、C 和 D 的鎖,則這一技術可能導致阻塞,但它永遠不會在這四個鎖上造成死鎖。

活鎖
當一個執行緒忙於接受新任務以致它永遠沒有機會完成任何任務時,就會發生活鎖。這個執行緒最終將超出緩衝區並導致程式崩潰。試想一個祕書需要錄入一封信,但她一直在忙於接電話,所以這封信永遠不會被錄入。

記憶體損壞
如果明智地使用 synchronized 關鍵字,則完全可以避免記憶體錯誤這種氣死人的問題。

資源耗盡
某些系統資源是有限的,如檔案描述符。多執行緒程式可能耗盡資源,因為每個執行緒都可能希望有一個這樣的資源。如果執行緒數相當大,或者某個資源的侯選執行緒數遠遠超過了可用的資源數,則最好使用 資源池。一個最好的示例是資料庫連線池。只要執行緒需要使用一個資料庫連線,它就從池中取出一個,使用以後再將它返回池中。資源池也稱為 資源庫





回頁首

除錯大量的執行緒

有時一個程式因為有大量的執行緒在執行而極難除錯。在這種情況下,下面的這個類可能會派上用場:

public class Probe extends Thread {
    public Probe() {}
    public void run() {
        while(true) {
            Thread[] x = new Thread[100];
            Thread.enumerate(x);
            for(int i=0; i<100; i++) {
            Thread t = x[i];
            if(t == null)
                break;
            else
                System.out.println(t.getName() + "\t" + t.getPriority()
                + "\t" + t.isAlive() + "\t" + t.isDaemon());
            }
        }
    }
}





回頁首

限制執行緒優先順序和排程

Java 執行緒模型涉及可以動態更改的執行緒優先順序。本質上,執行緒的優先順序是從 1 到 10 之間的一個數字,數字越大表明任務越緊急。JVM 標準首先呼叫優先順序較高的執行緒,然後才呼叫優先順序較低的執行緒。但是,該標準對具有相同優先順序的執行緒的處理是隨機的。如何處理這些執行緒取決於基層的作業系統策略。在某些情況下,優先順序相同的執行緒分時執行;在另一些情況下,執行緒將一直執行到結束。請記住,Java 支援 10 個優先順序,基層作業系統支援的優先順序可能要少得多,這樣會造成一些混亂。因此,只能將優先順序作為一種很粗略的工具使用。最後的控制可以通過明智地使用 yield() 函式來完成。通常情況下,請不要依靠執行緒優先順序來控制執行緒的狀態。





回頁首

小結

本文說明了在 Java 程式中如何使用執行緒。像是否 應該使用執行緒這樣的更重要的問題在很大程式上取決於手頭的應用程式。決定是否在應用程式中使用多執行緒的一種方法是,估計可以並行執行的程式碼量。並記住以下幾點:

  • 使用多執行緒不會增加 CPU 的能力。但是如果使用 JVM 的本地執行緒實現,則不同的執行緒可以在不同的處理器上同時執行(在多 CPU 的機器中),從而使多 CPU 機器得到充分利用。
  • 如果應用程式是計算密集型的,並受 CPU 功能的制約,則只有多 CPU 機器能夠從更多的執行緒中受益。
  • 當應用程式必須等待緩慢的資源(如網路連線或資料庫連線)時,或者當應用程式是非互動式的時,多執行緒通常是有利的。

基於 Internet 的軟體有必要是多執行緒的;否則,使用者將感覺應用程式反映遲鈍。例如,當開發要支援大量客戶機的伺服器時,多執行緒可以使程式設計較為容易。在這種情況下,每個執行緒可以為不同的客戶或客戶組服務,從而縮短了響應時間。

某些程式設計師可能在 C 和其他語言中使用過執行緒,在那些語言中對執行緒沒有語言支援。這些程式設計師可能通常都被搞得對執行緒失去了信心。 

相關文章