Java高階-解析Java中的多執行緒機制(轉)

BSDLite發表於2007-08-13
Java高階-解析Java中的多執行緒機制(轉)[@more@]程式與應用程式的區別



程式(Process)是最初定義在Unix等多使用者、多工作業系統環境下用於表示應用程式在記憶體環境中基本執行單元的概念。以Unix作業系統為例,程式是Unix作業系統環境中的基本成分、是系統資源分配的基本單位。Unix作業系統中完成的幾乎所有使用者管理和資源分配等工作都是透過作業系統對應用程式程式的控制來實現的。

C、C++、Java等語言編寫的源程式經相應的編譯器編譯成可執行檔案後,提交給計算機處理器執行。這時,處在可執行狀態中的應用程式稱為程式。從使用者角度來看,程式是應用程式的一個執行過程。從作業系統核心角度來看,程式代表的是作業系統分配的記憶體、CPU時間片等資源的基本單位,是為正在執行的程式提供的執行環境。程式與應用程式的區別在於應用程式作為一個靜態檔案儲存在計算機系統的硬碟等儲存空間中,而程式則是處於動態條件下由作業系統維護的系統資源管理實體。多工環境下應用程式程式的主要特點包括:

●程式在執行過程中有記憶體單元的初始入口點,並且程式存活過程中始終擁有獨立的記憶體地址空間;

●程式的生存期狀態包括建立、就緒、執行、阻塞和死亡等型別;

●從應用程式程式在執行過程中向CPU發出的執行指令形式不同,可以將程式的狀態分為使用者態和核心態。處於使用者態下的程式執行的是應用程式指令、處於核心態下的應用程式程式執行的是作業系統指令。

在Unix作業系統啟動過程中,系統自動建立swapper、init等系統程式,用於管理記憶體資源以及對使用者程式進行排程等。在Unix環境下無論是由作業系統建立的程式還要由應用程式執行建立的程式,均擁有唯一的程式標識(PID)。

程式與Java執行緒的區別

應用程式在執行過程中存在一個記憶體空間的初始入口點地址、一個程式執行過程中的程式碼執行序列以及用於標識程式結束的記憶體出口點地址,在程式執行過程中的每一時間點均有唯一的處理器指令與記憶體單元地址相對應。

Java語言中定義的執行緒(Thread)同樣包括一個記憶體入口點地址、一個出口點地址以及能夠順序執行的程式碼序列。但是程式與執行緒的重要區別在於執行緒不能夠單獨執行,它必須執行在處於活動狀態的應用程式程式中,因此可以定義執行緒是程式內部的具有併發性的順序程式碼流。

Unix 作業系統和Microsoft Windows作業系統支援多使用者、多程式的併發執行,而Java語言支援應用程式程式內部的多個執行執行緒的併發執行。多執行緒的意義在於一個應用程式的多個邏輯單元可以併發地執行。但是多執行緒並不意味著多個使用者程式在執行,作業系統也不把每個執行緒作為獨立的程式來分配獨立的系統資源。程式可以建立其子程式,子程式與父程式擁有不同的可執行程式碼和資料記憶體空間。而在用於代表應用程式的程式中多個執行緒共享資料記憶體空間,但保持每個執行緒擁有獨立的執行堆疊和程式執行上下文(Context)。

基於上述區別,執行緒也可以稱為輕型程式 (Light Weight Process,LWP)。不同執行緒間允許任務協作和資料交換,使得在計算機系統資源消耗等方面非常廉價。

執行緒需要作業系統的支援,不是所有型別的計算機都支援多執行緒應用程式。Java程式設計語言將執行緒支援與語言執行環境結合在一起,提供了多工併發執行的能力。這就好比一個人在處理家務的過程中,將衣服放到洗衣機中自動洗滌後將大米放在電飯鍋裡,然後開始做菜。等菜做好了,飯熟了同時衣服也洗好了。

需要注意的是:在應用程式中使用多執行緒不會增加 CPU 的資料處理能力。只有在多CPU 的計算機或者在網路計算體系結構下,將Java程式劃分為多個併發執行執行緒後,同時啟動多個執行緒執行,使不同的執行緒執行在基於不同處理器的Java虛擬機器中,才能提高應用程式的執行效率。

另外,如果應用程式必須等待網路連線或資料庫連線等資料吞吐速度相對較慢的資源時,多執行緒應用程式是非常有利的。基於Internet的應用程式有必要是多執行緒型別的,例如,當開發要支援大量客戶機的伺服器端應用程式時,可以將應用程式建立成多執行緒形式來響應客戶端的連線請求,使每個連線使用者獨佔一個客戶端連線執行緒。這樣,使用者感覺伺服器只為連線使用者自己服務,從而縮短了伺服器的客戶端響應時間。

Java語言的多執行緒程式設計方法



利用Java語言實現多執行緒應用程式的方法很簡單。根據多執行緒應用程式繼承或實現物件的不同可以採用兩種方式:一種是應用程式的併發執行物件直接繼承Java的執行緒類Thread;另外一種方式是定義併發執行物件實現Runnable介面。

繼承Thread類的多執行緒程式設計方法

Thread 類是JDK中定義的用於控制執行緒物件的類,在該類中封裝了用於進行執行緒控制的方法。見下面的示例程式碼:

//Consumer.java

import java.util.*;

class Consumer extends Thread

{

int nTime;

String strConsumer;

public Consumer(int nTime, String strConsumer)

{ this.nTime = nTime;

this.strConsumer = strConsumer;

}

public void run()

{

while(true)

{ try

{

System.out.println("Consumer name:"+strConsumer+" ");

Thread.sleep(nTime);

}

catch(Exception e)

{

e.printStackTrace();

}  } }

static public void main(String args[])

{ Consumer aConsumer = new Consumer (1000, "aConsumer");

aConsumer.start();

Consumer bConsumer = new Consumer (2000, "bConsumer");

bConsumer.start();

Consumer cConsumer = new Consumer (3000, "cConsumer ");

cConsumer.start();

} }

從上面的程式程式碼可以看出:多執行緒執行地下Consumer繼承Java語言中的執行緒類Thread並且在main方法中建立了三個Consumer物件的例項。當呼叫物件例項的start方法時,自動呼叫Consumer類中定義的run方法啟動物件執行緒執行。執行緒執行的結果是每間隔nTime時間列印出物件例項中的字串成員變數strConsumer的內容。

可以總結出繼承Thread類的多執行緒程式設計方法是使應用程式類繼承Thread類並且在該類的run方法中實現併發性處理過程。



實現Runnable介面的多執行緒程式設計方法



Java語言中提供的另外一種實現多執行緒應用程式的方法是多執行緒物件實現Runnable介面並且在該類中定義用於啟動執行緒的run方法。這種定義方式的好處在於多執行緒應用物件可以繼承其它物件而不是必須繼承Thread類,從而能夠增加類定義的邏輯性。

實現Runnable介面的多執行緒應用程式框架程式碼如下所示:

//Consumer.java

import java.util.*;

class Consumer implements Runnable

{

… …

public Consumer(int nTime, String strConsumer){… …}

public void run(){… …}

static public void main(String args[])

{

Thread aConsumer = new Thread(new Consumer(1000, "aConsumer"));

aConsumer.start();

//其它物件例項的執行執行緒

//… …

} }

從上述程式碼可以看出:該類實現了Runnable介面並且在該類中定義了run方法。這種多執行緒應用程式的實現方式與繼承Thread類的多執行緒應用程式的重要區別在於啟動多執行緒物件的方法設計方法不同。在上述程式碼中,透過建立Thread物件例項並且將應用物件作為建立Thread類例項的引數。

執行緒間的同步

Java 應用程式的多個執行緒共享同一程式的資料資源,多個使用者執行緒在併發執行過程中可能同時訪問具有敏感性的內容。在Java中定義了執行緒同步的概念,實現對共享資源的一致性維護。下面以筆者最近開發的行動通訊計費系統中執行緒間同步控制方法,說明Java語言中多執行緒同步方式的實現過程。

在沒有多執行緒同步控制策略條件下的客戶賬戶類定義框架程式碼如下所示:

public class RegisterAccount

{float fBalance;

//客戶繳費方法

public void deposit(float fFees){ fBalance += fFees; }

//通話計費方法

public void withdraw(float fFees){ fBalance -= fFees; }

… …

}  

讀者也許會認為:上述程式程式碼完全能夠滿足計費系統實際的需要。確實,在單執行緒環境下該程式確實是可靠的。但是,多程式併發執行的情況是怎樣的呢?假設發生這種情況:客戶在客戶服務中心進行繳費的同時正在利用行動通訊裝置僅此通話,客戶通話結束時計費系統啟動計費程式,而同時服務中心的工作人員也提交繳費程式執行。讀者可以看到如果發生這種情況,對客戶賬戶的處理是不嚴肅的。

如何解決這種問題呢?很簡單,在 RegisterAccount類方法定義中加上用於標識同步方法的關鍵字synchronized。這樣,在同步方法執行過程中該方法涉及的共享資源(在上述程式碼中為fBalance成員變數)將被加上共享鎖,以確保在方法執行期間只有該方法能夠對共享資源進行訪問,直到該方法的執行緒執行結束開啟共享鎖,其它執行緒才能夠訪問這些共享資源。在共享鎖沒有開啟的時候其它訪問共享資源的執行緒處於阻塞狀態。

進行執行緒同步策略控制後的RegisterAccount類定義如下面程式碼所示:

public class RegisterAccount

{float fBalance;

public synchronized void deposit(float fFees){ fBalance += fFees; }

public synchronized void withdraw(float fFees){ fBalance -= fFees; }

… …

}

從經過執行緒同步機制定義後的程式碼形式可以看出:在對共享資源進行訪問的方法訪問屬性關鍵字(public)後附加同步定義關鍵字synchronized,使得同步方法在對共享資源訪問的時候,為這些敏感資源附加共享鎖來控制方法執行期間的資源獨佔性,實現了應用系統資料資源的一致性管理和維護。

執行緒的狀態控制



在這裡需要明確的是:無論採用繼承Thread類還是實現Runnable介面來實現應用程式的多執行緒能力,都需要在該類中定義用於完成實際功能的run方法,這個run方法稱為執行緒體(Thread Body)。按照執行緒體在計算機系統記憶體中的狀態不同,可以將執行緒分為建立、就緒、執行、睡眠、掛起和死亡等型別。這些執行緒狀態型別下執行緒的特徵為:

建立狀態:當利用new關鍵字建立執行緒物件例項後,它僅僅作為一個物件例項存在,JVM沒有為其分配CPU時間片等執行緒執行資源;

就緒狀態:在處於建立狀態的執行緒中呼叫start方法將執行緒的狀態轉換為就緒狀態。這時,執行緒已經得到除CPU時間之外的其它系統資源,只等JVM的執行緒排程器按照執行緒的優先順序對該執行緒進行排程,從而使該執行緒擁有能夠獲得CPU時間片的機會。

睡眠狀態:線上程執行過程中可以呼叫sleep方法並在方法引數中指定執行緒的睡眠時間將執行緒狀態轉換為睡眠狀態。這時,該執行緒在不釋放佔用資源的情況下停止執行指定的睡眠時間。時間到達後,執行緒重新由JVM執行緒排程器進行排程和管理。

掛起狀態:可以透過呼叫suspend方法將執行緒的狀態轉換為掛起狀態。這時,執行緒將釋放佔用的所有資源,由JVM排程轉入臨時儲存空間,直至應用程式呼叫resume方法恢復執行緒執行。

死亡狀態:當執行緒體執行結束或者呼叫執行緒物件的stop方法後執行緒將終止執行,由JVM收回執行緒佔用的資源。

在Java執行緒類中分別定義了相應的方法,用於在應用程式中對執行緒狀態進行控制和管理。

執行緒的排程

執行緒呼叫的意義在於JVM應對執行的多個執行緒進行系統級的協調,以避免多個執行緒爭用有限資源而導致應用系統當機或者崩潰。

為了執行緒對於作業系統和使用者的重要性區分開,Java定義了執行緒的優先順序策略。Java將執行緒的優先順序分為10個等級,分別用1-10之間的數字表示。數字越大表明執行緒的級別越高。相應地,在Thread類中定義了表示執行緒最低、最高和普通優先順序的成員變數MIN_PRIORITY、 MAX_PRIORITY和NORMAL_PRIORITY,代表的優先順序等級分別為1、10和5。當一個執行緒物件被建立時,其預設的執行緒優先順序是5。

為了控制執行緒的執行策略,Java定義了執行緒排程器來監控系統中處於就緒狀態的所有執行緒。執行緒排程器按照執行緒的優先順序決定那個執行緒投入處理器執行。在多個執行緒處於就緒狀態的條件下,具有高優先順序的執行緒會在低優先順序執行緒之前得到執行。執行緒排程器同樣採用"搶佔式"策略來排程執行緒執行,即當前執行緒執行過程中有較高優先順序的執行緒進入就緒狀態,則高優先順序的執行緒立即被排程執行。具有相同優先順序的所有執行緒採用輪轉的方式來共同分配CPU時間片。

在應用程式中設定執行緒優先順序的方法很簡單,在建立執行緒物件之後可以呼叫執行緒物件的setPriority方法改變該執行緒的執行優先順序,同樣可以呼叫getPriority方法獲取當前執行緒的優先順序。

在Java 中比較特殊的執行緒是被稱為守護(Daemon)執行緒的低階別執行緒。這個執行緒具有最低的優先順序,用於為系統中的其它物件和執行緒提供服務。將一個使用者執行緒設定為守護執行緒的方式是線上程物件建立之前呼叫執行緒物件的setDaemon方法。典型的守護執行緒例子是JVM中的系統資源自動回收執行緒,它始終在低階別的狀態中執行,用於實時監控和管理系統中的可回收資源。

執行緒分組管理

Java定義了在多執行緒執行系統中的執行緒組(ThreadGroup)物件,用於實現按照特定功能對執行緒進行集中式分組管理。使用者建立的每個執行緒均屬於某執行緒組,這個執行緒組可以線上程建立時指定,也可以不指定執行緒組以使該執行緒處於預設的執行緒組之中。但是,一旦執行緒加入某執行緒組,該執行緒就一直存在於該執行緒組中直至執行緒死亡,不能在中途改變執行緒所屬的執行緒組。

當Java的Application應用程式執行時,JVM建立名稱為main的執行緒組。除非單獨指定,在該應用程式中建立的執行緒均屬於main執行緒組。在main執行緒組中可以建立其它名稱的執行緒組並將其它執行緒加入到該執行緒組中,依此類推,構成執行緒和執行緒組之間的樹型管理和繼承關係。

與執行緒類似,可以針對執行緒組物件進行執行緒組的排程、狀態管理以及優先順序設定等。在對執行緒組進行管理過程中,加入到某執行緒組中的所有執行緒均被看作統一的物件。

小結

本文針對Java平臺中執行緒的性質和應用程式的多執行緒策略進行了分析和講解。與其它作業系統環境不同,Java執行環境中的執行緒類似於多使用者、多工作業系統環境下的程式,但在程式和執行緒的執行及建立方式等方面,程式與Java執行緒具有明顯區別。

Unix 作業系統環境下,應用程式可以利用fork函式建立子程式,但子程式與該應用程式程式擁有獨立的地址空間、系統資源和程式碼執行單元,並且程式的排程是由作業系統來完成的,使得在應用程式之間進行通訊和執行緒協調相對複雜。而Java應用程式中的多執行緒則是共享同一應用系統資源的多個並行程式碼執行體,執行緒之間的通訊和協調方法相對簡單。

可以說:Java語言對應用程式多執行緒能力的支援增強了Java作為網路程式設計語言的優勢,為實現分散式應用系統中多客戶端的併發訪問以及提高伺服器的響應效率奠定堅實基礎。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10617542/viewspace-955476/,如需轉載,請註明出處,否則將追究法律責任。

相關文章