|
Java 程序中的多線(xiàn)程(下篇)
synchronized 關(guān)鍵字
到目前為止,我們看到的示例都只是以非常簡(jiǎn)單的方式來(lái)利用線(xiàn)程。只有最小的數(shù)據(jù)流,而且不會(huì)出現(xiàn)兩個(gè)線(xiàn)程訪(fǎng)問(wèn)同一個(gè)對(duì)象的情況。但是,在大多數(shù)有用的程序中,線(xiàn)程之間通常有信息流。試考慮一個(gè)金融應(yīng)用程序,它有一個(gè) Account 對(duì)象,如下例中所示:
一個(gè)銀行中的多項(xiàng)活動(dòng)
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; } }
在此代碼樣例中潛伏著一個(gè)錯(cuò)誤。如果此類(lèi)用于單線(xiàn)程應(yīng)用程序,不會(huì)有任何問(wèn)題。但是,在多線(xiàn)程應(yīng)用程序的情況中,不同的線(xiàn)程就有可能同時(shí)訪(fǎng)問(wèn)同一個(gè) Account 對(duì)象,比如說(shuō)一個(gè)聯(lián)合帳戶(hù)的所有者在不同的 ATM 上同時(shí)進(jìn)行訪(fǎng)問(wèn)。在這種情況下,存入和支出就可能以這樣的方式發(fā)生:一個(gè)事務(wù)被另一個(gè)事務(wù)覆蓋。這種情況將是災(zāi)難性的。但是,Java 編程語(yǔ)言提供了一種簡(jiǎn)單的機(jī)制來(lái)防止發(fā)生這種覆蓋。每個(gè)對(duì)象在運(yùn)行時(shí)都有一個(gè)關(guān)聯(lián)的鎖。這個(gè)鎖可通過(guò)為方法添加關(guān)鍵字 synchronized 來(lái)獲得。這樣,修訂過(guò)的 Account 對(duì)象(如下所示)將不會(huì)遭受像數(shù)據(jù)損壞這樣的錯(cuò)誤:
對(duì)一個(gè)銀行中的多項(xiàng)活動(dòng)進(jìn)行同步處理
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() 函數(shù)都需要這個(gè)鎖來(lái)進(jìn)行操作,所以當(dāng)一個(gè)函數(shù)運(yùn)行時(shí),另一個(gè)函數(shù)就被阻塞。請(qǐng)注意,checkBalance() 未作更改,它嚴(yán)格是一個(gè)讀函數(shù)。因?yàn)?checkBalance() 未作同步處理,所以任何其他方法都不會(huì)阻塞它,它也不會(huì)阻塞任何其他方法,不管那些方法是否進(jìn)行了同步處理。 Java 編程語(yǔ)言中的高級(jí)多線(xiàn)程支持
線(xiàn)程組
線(xiàn)程是被個(gè)別創(chuàng)建的,但可以將它們歸類(lèi)到線(xiàn)程組中,以便于調(diào)試和監(jiān)視。只能在創(chuàng)建線(xiàn)程的同時(shí)將它與一個(gè)線(xiàn)程組相關(guān)聯(lián)。在使用大量線(xiàn)程的程序中,使用線(xiàn)程組組織線(xiàn)程可能很有幫助?梢詫⑺鼈兛醋魇怯(jì)算機(jī)上的目錄和文件結(jié)構(gòu)。
線(xiàn)程間發(fā)信 當(dāng)線(xiàn)程在繼續(xù)執(zhí)行前需要等待一個(gè)條件時(shí),僅有 synchronized 關(guān)鍵字是不夠的。雖然 synchronized 關(guān)鍵字阻止并發(fā)更新一個(gè)對(duì)象,但它沒(méi)有實(shí)現(xiàn)線(xiàn)程間發(fā)信。Object 類(lèi)為此提供了三個(gè)函數(shù):wait()、notify() 和 notifyAll()。以全球氣候預(yù)測(cè)程序?yàn)槔_@些程序通過(guò)將地球分為許多單元,在每個(gè)循環(huán)中,每個(gè)單元的計(jì)算都是隔離進(jìn)行的,直到這些值趨于穩(wěn)定,然后相鄰單元之間就會(huì)交換一些數(shù)據(jù)。所以,從本質(zhì)上講,在每個(gè)循環(huán)中各個(gè)線(xiàn)程都必須等待所有線(xiàn)程完成各自的任務(wù)以后才能進(jìn)入下一個(gè)循環(huán)。這個(gè)模型稱(chēng)為屏蔽同步,下例說(shuō)明了這個(gè)模型:
屏蔽同步 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(); } } }
當(dāng)對(duì)一個(gè)線(xiàn)程調(diào)用 wait() 時(shí),該線(xiàn)程就被有效阻塞,只到另一個(gè)線(xiàn)程對(duì)同一個(gè)對(duì)象調(diào)用 notify() 或 notifyAll() 為止。因此,在前一個(gè)示例中,不同的線(xiàn)程在完成它們的工作以后將調(diào)用 waitForAll() 函數(shù),最后一個(gè)線(xiàn)程將觸發(fā) notifyAll() 函數(shù),該函數(shù)將釋放所有的線(xiàn)程。第三個(gè)函數(shù) notify() 只通知一個(gè)正在等待的線(xiàn)程,當(dāng)對(duì)每次只能由一個(gè)線(xiàn)程使用的資源進(jìn)行訪(fǎng)問(wèn)限制時(shí),這個(gè)函數(shù)很有用。但是,不可能預(yù)知哪個(gè)線(xiàn)程會(huì)獲得這個(gè)通知,因?yàn)檫@取決于 Java 虛擬機(jī) (JVM) 調(diào)度算法。
將 CPU 讓給另一個(gè)線(xiàn)程
當(dāng)線(xiàn)程放棄某個(gè)稀有的資源(如數(shù)據(jù)庫(kù)連接或網(wǎng)絡(luò)端口)時(shí),它可能調(diào)用 yield() 函數(shù)臨時(shí)降低自己的優(yōu)先級(jí),以便某個(gè)其他線(xiàn)程能夠運(yùn)行。
守護(hù)線(xiàn)程
有兩類(lèi)線(xiàn)程:用戶(hù)線(xiàn)程和守護(hù)線(xiàn)程。用戶(hù)線(xiàn)程是那些完成有用工作的線(xiàn)程。守護(hù)線(xiàn)程是那些僅提供輔助功能的線(xiàn)程。Thread 類(lèi)提供了 setDaemon() 函數(shù)。Java 程序?qū)⑦\(yùn)行到所有用戶(hù)線(xiàn)程終止,然后它將破壞所有的守護(hù)線(xiàn)程。在 Java 虛擬機(jī) (JVM) 中,即使在 main 結(jié)束以后,如果另一個(gè)用戶(hù)線(xiàn)程仍在運(yùn)行,則程序仍然可以繼續(xù)運(yùn)行。
避免不提倡使用的方法
不提倡使用的方法是為支持向后兼容性而保留的那些方法,它們?cè)谝院蟮陌姹局锌赡艹霈F(xiàn),也可能不出現(xiàn)。Java 多線(xiàn)程支持在版本 1.1 和版本 1.2 中做了重大修訂,stop()、suspend() 和 resume() 函數(shù)已不提倡使用。這些函數(shù)在 JVM 中可能引入微妙的錯(cuò)誤。雖然函數(shù)名可能聽(tīng)起來(lái)很誘人,但請(qǐng)抵制誘惑不要使用它們。
調(diào)試線(xiàn)程化的程序
在線(xiàn)程化的程序中,可能發(fā)生的某些常見(jiàn)而討厭的情況是死鎖、活鎖、內(nèi)存損壞和資源耗盡。
死鎖
死鎖可能是多線(xiàn)程程序最常見(jiàn)的問(wèn)題。當(dāng)一個(gè)線(xiàn)程需要一個(gè)資源而另一個(gè)線(xiàn)程持有該資源的鎖時(shí),就會(huì)發(fā)生死鎖。這種情況通常很難檢測(cè)。但是,解決方案卻相當(dāng)好:在所有的線(xiàn)程中按相同的次序獲取所有資源鎖。例如,如果有四個(gè)資源 —A、B、C 和 D — 并且一個(gè)線(xiàn)程可能要獲取四個(gè)資源中任何一個(gè)資源的鎖,則請(qǐng)確保在獲取對(duì) B 的鎖之前首先獲取對(duì) A 的鎖,依此類(lèi)推。如果“線(xiàn)程 1”希望獲取對(duì) B 和 C 的鎖,而“線(xiàn)程 2”獲取了 A、C 和 D 的鎖,則這一技術(shù)可能導(dǎo)致阻塞,但它永遠(yuǎn)不會(huì)在這四個(gè)鎖上造成死鎖。
活鎖
當(dāng)一個(gè)線(xiàn)程忙于接受新任務(wù)以致它永遠(yuǎn)沒(méi)有機(jī)會(huì)完成任何任務(wù)時(shí),就會(huì)發(fā)生活鎖。這個(gè)線(xiàn)程最終將超出緩沖區(qū)并導(dǎo)致程序崩潰。試想一個(gè)秘書(shū)需要錄入一封信,但她一直在忙于接電話(huà),所以這封信永遠(yuǎn)不會(huì)被錄入。
內(nèi)存損壞
如果明智地使用 synchronized 關(guān)鍵字,則完全可以避免內(nèi)存錯(cuò)誤這種氣死人的問(wèn)題。
資源耗盡
某些系統(tǒng)資源是有限的,如文件描述符。多線(xiàn)程程序可能耗盡資源,因?yàn)槊總(gè)線(xiàn)程都可能希望有一個(gè)這樣的資源。如果線(xiàn)程數(shù)相當(dāng)大,或者某個(gè)資源的侯選線(xiàn)程數(shù)遠(yuǎn)遠(yuǎn)超過(guò)了可用的資源數(shù),則最好使用資源池。一個(gè)最好的示例是數(shù)據(jù)庫(kù)連接池。只要線(xiàn)程需要使用一個(gè)數(shù)據(jù)庫(kù)連接,它就從池中取出一個(gè),使用以后再將它返回池中。資源池也稱(chēng)為資源庫(kù)。
調(diào)試大量的線(xiàn)程
有時(shí)一個(gè)程序因?yàn)橛写罅康木(xiàn)程在運(yùn)行而極難調(diào)試。在這種情況下,下面的這個(gè)類(lèi)可能會(huì)派上用場(chǎng): 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()); } } } }
限制線(xiàn)程優(yōu)先級(jí)和調(diào)度
Java 線(xiàn)程模型涉及可以動(dòng)態(tài)更改的線(xiàn)程優(yōu)先級(jí)。本質(zhì)上,線(xiàn)程的優(yōu)先級(jí)是從 1 到 10 之間的一個(gè)數(shù)字,數(shù)字越大表明任務(wù)越緊急。JVM 標(biāo)準(zhǔn)首先調(diào)用優(yōu)先級(jí)較高的線(xiàn)程,然后才調(diào)用優(yōu)先級(jí)較低的線(xiàn)程。但是,該標(biāo)準(zhǔn)對(duì)具有相同優(yōu)先級(jí)的線(xiàn)程的處理是隨機(jī)的。如何處理這些線(xiàn)程取決于基層的操作系統(tǒng)策略。在某些情況下,優(yōu)先級(jí)相同的線(xiàn)程分時(shí)運(yùn)行;在另一些情況下,線(xiàn)程將一直運(yùn)行到結(jié)束。請(qǐng)記住,Java 支持 10 個(gè)優(yōu)先級(jí),基層操作系統(tǒng)支持的優(yōu)先級(jí)可能要少得多,這樣會(huì)造成一些混亂。因此,只能將優(yōu)先級(jí)作為一種很粗略的工具使用。最后的控制可以通過(guò)明智地使用 yield() 函數(shù)來(lái)完成。通常情況下,請(qǐng)不要依靠線(xiàn)程優(yōu)先級(jí)來(lái)控制線(xiàn)程的狀態(tài)。
小結(jié)
本文說(shuō)明了在 Java 程序中如何使用線(xiàn)程。像是否應(yīng)該使用線(xiàn)程這樣的更重要的問(wèn)題在很大程序上取決于手頭的應(yīng)用程序。決定是否在應(yīng)用程序中使用多線(xiàn)程的一種方法是,估計(jì)可以并行運(yùn)行的代碼量。并記住以下幾點(diǎn):
1、使用多線(xiàn)程不會(huì)增加 CPU 的能力。但是如果使用 JVM 的本地線(xiàn)程實(shí)現(xiàn),則不同的線(xiàn)程可以在不同的處理器上同時(shí)運(yùn)行(在多 CPU 的機(jī)器中),從而使多 CPU 機(jī)器得到充分利用。如果應(yīng)用程序是計(jì)算密集型的,并受 CPU 功能的制約,則只有多 CPU 機(jī)器能夠從更多的線(xiàn)程中受益。 當(dāng)應(yīng)用程序必須等待緩慢的資源(如網(wǎng)絡(luò)連接或數(shù)據(jù)庫(kù)連接)時(shí),或者當(dāng)應(yīng)用程序是非交互式的時(shí),多線(xiàn)程通常是有利的。
2、基于 Internet 的軟件有必要是多線(xiàn)程的;否則,用戶(hù)將感覺(jué)應(yīng)用程序反映遲鈍。例如,當(dāng)開(kāi)發(fā)要支持大量客戶(hù)機(jī)的服務(wù)器時(shí),多線(xiàn)程可以使編程較為容易。在這種情況下,每個(gè)線(xiàn)程可以為不同的客戶(hù)或客戶(hù)組服務(wù),從而縮短了響應(yīng)時(shí)間。
3、某些程序員可能在 C 和其他語(yǔ)言中使用過(guò)線(xiàn)程,在那些語(yǔ)言中對(duì)線(xiàn)程沒(méi)有語(yǔ)言支持。這些程序員可能通常都被搞得對(duì)線(xiàn)程失去了信心。
|
溫馨提示:喜歡本站的話(huà),請(qǐng)收藏一下本站!