<--在線程基礎的第二部分中,我們將了解一下使用Java線程的缺陷和副作用,以及在SUN JDK 1.2中是如何修改線程的運行機制的-->
在上篇文章《Java 101之線程基礎》中,我們介紹了線程的概念以及如何使用線程。這個月,我們將轉到更高級的話題,包括線程的缺陷及副作用,以及在SUN JDK 1.2中,是如何改進線程的運行機制的。
synchronize(同步)
讓我們回憶一下上篇文章中講的:線程允許兩個或者更多個進程同時執行。實際上,這些線程也可以共享對象和數據,在這種情形下,你要知道不同的線程在同一時間內不能存取同一數據,因為一開始設計Java的時候,就采用了線程的概念,Java語言定義了一個特殊的關鍵字synchronize(同步),該關鍵字可以應用到代碼塊上,代碼塊也包括入口方法,該關鍵字的目的是防止多個線程在同一時間執行同一代碼塊內的代碼。
定義一個同步的方法,格式如下:
[public|private] synchronized {type}
methodname(...)
一個把同步這個關鍵字應用到方法中的簡單的例子:
public class someClass {
public void aMethod() {
// Some code
synchronized(this) {
// Synchronized code block
}
// more code.
}
}
同步化的關鍵字可以保證在同一時間內只有一個線程可以執行該代碼段,而任何其他要用到該段代碼的線程將被阻塞,直到第一個線程執行完該段代碼。
死鎖和饑餓
對於饑餓的定義-由於別的並發的激活的過程持久占有所需資源,是莫個異步過程載客預測的時間內不能被激活。
最常遇到的線程的兩個缺陷是死鎖和饑餓。當一個或者多個進程,在一個給定的任務中,協同作用,互相干涉,而導致一個或者更多進程永遠等待下去,死鎖就發生了。與此類似,它當一個進程永久性地占有資源,使得其他進程得不到該資源,就發生了饑餓。
首先我們看一下死鎖問題。考慮一個簡單的例子,假如你到ATM機上取錢,但是你卻看到如下的信息“現在有沒有現金,請等會兒再試。”你需要錢,所以你就等了一會兒再試,但是你又看到同樣的信息。與此同時,在你後面,一輛運款裝甲車正等待著把錢放進ATM中,但是運款裝甲車到不了ATM取款機,因為你的汽車擋著道。而你又要取到錢,才會離開原地。這種情況下,就發生了死鎖。
在饑餓的情形下,系統不處於死鎖狀態中,因為有一個進程仍在處理之中,只是其他進程永遠得不到執行的機會。在什麼樣的環境下,會導致饑餓的發生,沒有預先確定好的規則。而一旦發生下面四種情況之一,就會導致死鎖的發生。
相互排斥: 一個線程或者進程永遠占有一共享資源,例如,獨占該資源。
循環等待: 進程A等待進程B,而後者又在等待進程C,而進程C又在等待進程A。
部分分配: 資源被部分分配。例如,進程A和B都需要用訪問一個文件,並且都要用到打印機,進程A獲得了文件資源,進程B獲得了打印機資源,但是兩個進程不能獲得全部的資源。
缺少優先權: 一個進程訪問了某個資源,但是一直不釋放該資源,即使該進程處於阻塞狀態。
如果上面四種情形都不出現,系統就不會發生死鎖。請再看一下剛才的文件/打印機的例子,當其中一個進程判斷出它得不到它所需要的第二個資源,就釋放已經得到的第一個資源,那麼第二個教程可以獲得兩個資源,並能夠運行下去。
線程的高級用法
到目前為止,我們已經談到創建和管理線程的基本知識。你需要做的就是啟動一個線程,並讓它運行。你的應用程序也許希望等待一個線程執行完畢,也許打算發送一個信息給線程,或者只打算讓線程在處理之前休眠一會兒。線程類提供了四種對線程進行操作的API調用。
Join
如果一個應用程序需要執行很多時間,比如一個耗時很長的計算工作,你可以把該計算工作設計成線程。但是,假定還有另外一個線程需要計算結果,當計算結果出來後,如何讓那個線程知道計算結果呢?解決該問題的一個方法是讓第二個線程一直不停地檢查一些變量的狀態,直到這些變量的狀態發生改變。這樣的方式在UNIX風格的服務器中常常用到。Java提供了一個更加簡單的機制,即線程類中的join 方法。
join 方法使得一個線程等待另外一個線程的結束。例如,一個GUI (或者其他線程)使用join方法等待一個子線程執行完畢:
CompleteCalcThread t = new
CompleteCalcThread();
t.start();
//
// 做一會兒其他的事情
// 然後等待
t.join();
// 使用計算結果...
你可以看到,用對子線程使用join方法,等待子線程執行完畢。 Join 有三種格式:
void join(): 等待線程執行完畢。
void join(long timeout): 最多等待某段時間讓線程完成。
void join(long milliseconds, int nanoseconds): 最多等待某段時間(毫秒+納秒),讓線程完成。
線程API isAlive同join相關聯時,是很有用的。一個線程在start(此時run方法已經啟動)之後,在stop之前的某時刻處於isAlive 狀態。
對於編寫線程的程序員來說,還有其他兩個有用的線程API,即wait和 notify。使用這兩個API,我們可以精確地控制線程的執行過程。考慮一個簡單的例子,有個生產者線程和消費者線程,為了讓應用程序更有效率,所以我們不打算采用查詢等待的方法。當消費者可以消費對象時,消費者需要得知該信息。
我們可以把該例子闡述如下:生產者線程不斷地在運行著,把項目放入列表中,該列表的add方法由synchronize 關鍵字保護著,當一個對象添加到列表中,生產者就可以通知消費者:它已經添加一個對象,消費者可以消費該對象了。每個對象的run方法的偽代碼請見表A。
表A: 演示高級線程方法的偽代碼
class ProdCons {
class List {
public synchronized boolean add(Object o)
{...}
public synchronized boleanremove (Object o)
{...}
}
List data = new List();
ProdThread producer = null;
ConsThread consumer = null;
ProdCons() {
producer = new ProdThread(this);
consumer = new ConsThread(this);
producer.start();
consumer.start();
}
}
消費者和生產者的類,請見表B和表C。
表B: Class ConsThread
class ConsThread extends Thread {
ProdCons parent;
ConsThread(ProdCons parent) {
this.parent = parent;
}
public synchronized void canConsume() {
notify();
}
public void run() {
boolean consumed;
do {
synchronized(this) {
try { wait();}
catch (Exception e) { ; }
}
do {
String str = (String)parent.list.remove();
if ( null == str) {
consumed = false;
break;
}
consumed = true;
System.out.println("Consumer
=>consumed " + str);
}
while ( true );
}
while (consumed);
}
}
表C: Class ProdThread
class ProdThread extends Thread {
ProdCons parent;
ProdThread(ProdCons parent) {
this.parent = parent;
}
public void run() {
for ( int i = 0; i < 10; i++) {
String str = new String("ImAString" + i);
System.out.println("Producer produced " + str);
parent.list.add(str);
parent.consumer.canConsume();
}
parent.consumer.canConsume();
}
}
注意:notify和wait兩個API都必須位於同步化(synchronized)的方法中或者代碼塊中!
線程和Sun JDK 1.2
線程提供了一項很有價值的服務,極大地增強了Java程序設計語言的功能。然而,目前的線程實現的確存在一些問題。這些問題的存在,使得Sun JDK 1.2中線程的stop, suspend和resume方法導致人們的批評。
如果我們回到上面的生產者/消費者例子,我們就可以更好地理解這個問題。首先,我們看看死鎖。當運行一個applet小程序時,在通常的情況下,兩個線程運行時,相安無事,但是,但用戶點擊到另外一個網頁時,問題出現了。如果生產者正在添加一個項目到列表中,最壞的情況就是消費者線程被阻塞。假定,小程序正在創建一個對象,此時突然被掛起(suspended),其他的小程序就不能再對該數據進行更新。盡管出現這樣的機會不多,它們的確存在,有時會引起問題。
線程的第二個問題有關不一致的問題。再來看一下生產者/消費者的例子,不難想象,如果生產者線程在添加項目的過程中遇到被中止的情況,可能會造成列表狀態不一致。如果我們全面檢查現有的Java小程序的個數,就不難發現問題所在。
處理這個不一致的問題的最簡單的方法就是派生一個新的線程類,該線程類具有如下功能:通過一個方法的調用可以改變其狀態。表D就定義了這樣的一個類。MyThread類可以被掛起和重新執行,而無需擔心MyThread類的資源會崩潰。MyThread類中的方法 changeState用於暗示應該暫停,停止或者重新執行線程,而不同於以往的停止或者暫停線程。可以向線程發出請求,要求線程在合適的時候處理該請求,而不是強制處理該請求,因而無需向線程發出停止命令。
表D: Class MyThread
public class MyThread extends Thread {
//States the thread can be in.
static final int STATE_RUNNING = 0;
static final int STATE_STOP = 1;
static final int STATE_SUSPEND = 2;
private int currentState = STATE_RUNNING;
// The public method changeState allows
// another process to poke at that hread
// and tell it to do something when it
// next gets a chance.
public final synchronized void
changeState(int newState) {
currentState = newState;
if (STATE_RUNNING == currentState)
notify();
// Must have been suspended.
}
private synchronized boolean currentState() {
// If we where asked to suspend,
// just hang out until we are
// asked to either run or stop.
while ( STATE_SUSPEND == currentState) {
try{ wait(); }
catch (Exception e) {};
}
if ( STATE_STOP == currentState )
return false;
else
return true;
}
public void run() {
do {
if (currentState() == false)
return; // Done
// Perform some work
} while (true);
}
}
MyThread類的用戶可以重載run方法,然而,用戶需要檢查是否有另外的類請求線程改變狀態。在JDK 1.2 中對線程的運行機制所做的改變,是問題的症結所在,線程在運行時是否出現不一致,在線程關閉後,是否放棄所占用的資源,線程的運行是否正常,這些工作都是要開發者自己來確保完成了。
結論
線程功能強大而使用復雜。每位Java開發者可以在很多應用場合用到線程。本文中,我們檢查了線程的一些副作用,以及線程的一些高級用法。隨著Sun JDK 1.2的推出,開發者們將被迫對其編寫的線程對系統和其他進程的作用過程考慮得更加周到。最終,對於線程及其相關知識的正確理解,將會有助於聰明的開發者設計出更加健壯的應用程序。