多線程是Java程序設(shè)計(jì)語(yǔ)言的一個(gè)亮點(diǎn),它使用戶可以很方便地編寫(xiě)多線程程序,雖然編寫(xiě)多線程代碼需要考慮諸如安全、死鎖、資源共享的問(wèn)題,但是總體上講Java在編寫(xiě)多線程程序上比其他語(yǔ)言都要簡(jiǎn)潔。
使用多線程最直接的例子是具有用戶界面的程序。如果用戶界面上設(shè)計(jì)了一個(gè)按鈕,一旦單擊該按鈕程序會(huì)自動(dòng)在網(wǎng)絡(luò)上搜索指定數(shù)據(jù),當(dāng)然這個(gè)過(guò)程會(huì)持續(xù)一段時(shí)間。如果沒(méi)有多線程實(shí)現(xiàn)技術(shù),就會(huì)出現(xiàn)用戶界面無(wú)法控制的局面,即在網(wǎng)絡(luò)數(shù)據(jù)搜索完之前,用戶界面根本不響應(yīng)其他界面輸入。整個(gè)界面象是靜止在那里而無(wú)法操作。而我們希望不管系統(tǒng)當(dāng)前在完成什么任務(wù),都允許用戶操作界面元素,如查詢數(shù)據(jù),完成其他信息的處理等。這樣就要求程序可以同時(shí)執(zhí)行多個(gè)任務(wù),響應(yīng)用戶的不同操作請(qǐng)求。對(duì)于用戶而言就仿佛有多個(gè)處理器在為其工作。而在單處理器的計(jì)算機(jī)上完成程序的多任務(wù)功能就需要多線程技術(shù)。
多線程技術(shù)可以模擬多處理器的效果,對(duì)用戶而言計(jì)算機(jī)同時(shí)完成一個(gè)程序的多個(gè)任務(wù),而實(shí)際上該機(jī)制使得計(jì)算機(jī)把CPU周期按照一定策略分配給每一個(gè)線程,而高速的CPU使得用戶覺(jué)得計(jì)算機(jī)在同時(shí)完成多個(gè)任務(wù)。
9.1? 線程概述
線程是操作系統(tǒng)的概念,線程也稱之為輕量級(jí)進(jìn)程(lightweight process LWP),是CPU的基本使用單元,它的輕量級(jí)名稱是和進(jìn)程相關(guān)的。線程由線程ID、程序記數(shù)器、寄存器和堆棧組成,多個(gè)線程可以共享代碼段、數(shù)據(jù)段和諸如打開(kāi)的文件等的系統(tǒng)資源。而傳統(tǒng)的進(jìn)程其實(shí)就是單線程控制程序,每個(gè)進(jìn)程都有自己的代碼段、數(shù)據(jù)段和其他系統(tǒng)資源。這無(wú)疑使得每個(gè)進(jìn)程管理更多的內(nèi)容,從而稱為重量級(jí)進(jìn)程。“輕量”是指線程沒(méi)有獨(dú)自的存儲(chǔ)空間,和同一個(gè)進(jìn)程的多個(gè)線程共享存儲(chǔ)空間。
多線程和傳統(tǒng)的單線程在程序設(shè)計(jì)上的最大區(qū)別是每個(gè)線程獨(dú)自運(yùn)行,是彼此獨(dú)立的指令流,造成線程之間的執(zhí)行是亂序的,所以線程的控制需要謹(jǐn)慎對(duì)待。
下面分別詳細(xì)介紹進(jìn)程和線程的概念,如何創(chuàng)建線程、設(shè)置線程的優(yōu)先級(jí)、線程控制和線程同步等關(guān)鍵問(wèn)題。
9.2? 創(chuàng)建線程
在學(xué)習(xí)線程前,一定要先了解Java的線程機(jī)制,然后學(xué)習(xí)如何利用Thread類(lèi)實(shí)現(xiàn)多線程。Java的多線程機(jī)制提供了兩種方式實(shí)現(xiàn)多線程編程,一種是通過(guò)繼承java.long.Thread類(lèi)來(lái)實(shí)現(xiàn),一種是通過(guò)實(shí)現(xiàn)Runnable接口實(shí)現(xiàn)。
9.2.1? 繼承Thread類(lèi)創(chuàng)建線程
Thread類(lèi)是Java實(shí)現(xiàn)多線程的提供了簡(jiǎn)單的方法,Thread類(lèi)已經(jīng)具備了運(yùn)行多線程所需要的資源,用戶只需要重載該類(lèi)的run()方法,把需要使用多線程運(yùn)行的代碼放入該方法。這樣這些代碼就可以和其他線程“同時(shí)”存在。創(chuàng)建線程對(duì)象并用該對(duì)象調(diào)用start()方法則線程開(kāi)始運(yùn)行,start()方法提供了啟動(dòng)線程和線程運(yùn)行所需要的框架。
代碼是一個(gè)例子,說(shuō)明使用繼承 Thread類(lèi)實(shí)現(xiàn)多線程。每次new一個(gè)線程都設(shè)置一個(gè)線程計(jì)數(shù)器,表明建立的線程數(shù)。整個(gè)程序啟動(dòng)3個(gè)線程,每個(gè)線程會(huì)有9次輸出,但是三個(gè)線程的建立并非順序執(zhí)行,而每個(gè)線程的9次輸出也不一定會(huì)順序輸出。如代碼繼承Thread類(lèi)實(shí)現(xiàn)多線程示例所示。
9.2.2? 實(shí)現(xiàn)Runnable接口創(chuàng)建線程
Java提供了另一個(gè)有用的接口實(shí)現(xiàn)多線程編程。因?yàn)镴ava不支持多繼承,所以如果用戶的類(lèi)已經(jīng)繼承了一個(gè)類(lèi),而又需要多線程機(jī)制的支持,此時(shí)繼承thread類(lèi)就不現(xiàn)實(shí)了。所以Runnable接口在這種情況下就很實(shí)用。
Runnable 接口有唯一一個(gè)方法run(),所以實(shí)現(xiàn)該接口時(shí)必須自己定義該方法,提供多線程需要執(zhí)行的代碼。如果運(yùn)行通過(guò)實(shí)現(xiàn)Runnable接口的多線程程序,則需要借助Thread類(lèi),因?yàn)镽unnable接口沒(méi)有提供任何東西支持多線程,必須借助Thead類(lèi)的框架實(shí)現(xiàn)多線程,即通過(guò)類(lèi)Thread的構(gòu)造函數(shù) public Thread(Runnable target)來(lái)實(shí)現(xiàn)。代碼是通過(guò)繼承Runnable接口而實(shí)現(xiàn)多線程的例子。我們分析和運(yùn)行該程序,觀察輸出結(jié)果就可以很好的理解其運(yùn)用。
9.3? 線程的狀態(tài)
在Java中線程的執(zhí)行過(guò)程稍微有些復(fù)雜,但線程對(duì)象創(chuàng)建后并不不是立即執(zhí)行,需要做些準(zhǔn)備工作才有執(zhí)行的權(quán)利,而一旦搶占到CPU周期,則線程可以運(yùn)行,但CPU周期結(jié)束則線程必須暫時(shí)停止,或線程執(zhí)行過(guò)程中的某個(gè)條件無(wú)法滿足時(shí)也會(huì)暫時(shí)停止,只有等待條件滿足時(shí)才會(huì)繼續(xù)執(zhí)行,最后從run()方法返回后,線程退出。可以看出線程的執(zhí)行過(guò)程中涉及一些狀態(tài),線程就在這些狀態(tài)之間遷移。
做一點(diǎn)說(shuō)明,Java 規(guī)范中只定義了線程的四種狀態(tài),即新建狀態(tài)、可運(yùn)行狀態(tài)、阻塞狀態(tài)和死亡狀態(tài)。為了更清晰的說(shuō)明線程的狀態(tài)變化過(guò)程,我們認(rèn)為劃分為五個(gè)狀態(tài)更好理解,這里把可運(yùn)行狀態(tài)(Runnable)分解為就緒狀態(tài)和運(yùn)行狀態(tài),可以更好的理解可運(yùn)行狀態(tài)的含義。
線程包括五個(gè)狀態(tài):新建狀態(tài)、就緒狀態(tài)、運(yùn)行狀態(tài)、阻塞狀態(tài)和死亡狀態(tài)。下面分別詳細(xì)介紹這五種狀態(tài)。
9.4? 線程的優(yōu)先級(jí)
線程的有限級(jí)表示一個(gè)線程被CPU執(zhí)行的機(jī)會(huì)多少。注意,這里用“機(jī)會(huì)多少”來(lái)表達(dá)而不是用“先后順序”來(lái)表達(dá)。在Java中雖然定義了設(shè)置線程優(yōu)先級(jí)高低的方法,但是優(yōu)先級(jí)低并不意味著在不同優(yōu)先級(jí)的線程中就不會(huì)被執(zhí)行,優(yōu)先級(jí)低只說(shuō)明該線程被執(zhí)行的概率小,同理優(yōu)先級(jí)高的線程獲得CPU的概率就大。
通過(guò)Tread類(lèi)的setPriority()方法設(shè)置線程的優(yōu)先級(jí),該方法的參數(shù)為int型,其實(shí) Java提供了三個(gè)優(yōu)先級(jí)別,都為T(mén)hread類(lèi)的常量,從高到低依次為T(mén)hread. MAX-PRIORITY、Thread.NORM_PRIORITY、Thread.MIN_PRIORITY。這里再次重申,優(yōu)先級(jí)低并不意味著線程得不到執(zhí)行,而是線程被優(yōu)先執(zhí)行的概率小。這也說(shuō)明設(shè)置線程的優(yōu)先級(jí)不會(huì)造成死鎖的發(fā)生。
9.5? 線程的同步
在多線程中經(jīng)常遇到的一個(gè)問(wèn)題就是資源共享問(wèn)題,假設(shè)兩個(gè)線程同時(shí)訪問(wèn)一個(gè)數(shù)據(jù)區(qū),一個(gè)讀數(shù)據(jù)、一個(gè)寫(xiě)數(shù)據(jù),在一個(gè)線程讀數(shù)據(jù)前另一個(gè)線程修改了數(shù)據(jù),則讀數(shù)據(jù)線程讀到的不是原始數(shù)據(jù)而是被修改過(guò)的數(shù)據(jù),顯然這樣使不允許的。而在多線程編程中經(jīng)常會(huì)遇到訪問(wèn)共享資源的問(wèn)題,這些資源可以是數(shù)據(jù)、文件、一塊內(nèi)存區(qū)或是外圍設(shè)備的訪問(wèn)等。所以必須解決多線程編程中如何實(shí)現(xiàn)資源的共享問(wèn)題,在Java中稱為線程的同步問(wèn)題,在多數(shù)的編程語(yǔ)言中解決共享資源沖突的方法是采用順序機(jī)制(Serialize),通過(guò)為共享資源加鎖的方法實(shí)現(xiàn)資源的順序訪問(wèn)。
9.5.1? Java程序的資源共享
通過(guò)下面的例子,說(shuō)明如果沒(méi)有實(shí)現(xiàn)線程同步的訪問(wèn)共享資源會(huì)遇到的問(wèn)題。我們?cè)O(shè)計(jì)一個(gè)線程類(lèi) FooOne,該類(lèi)的多線程代碼無(wú)限循環(huán)的輸出一個(gè)值,每次循環(huán)該值遞增,但遞增到100時(shí),停止循環(huán)線程退出,這由方法run()中調(diào)用方法 printVal()實(shí)現(xiàn)。另一個(gè)線程類(lèi)FooTwo調(diào)用類(lèi)FooOne的對(duì)象,該類(lèi)的run()方法調(diào)用類(lèi)FooOne對(duì)象的printVal()方法,也實(shí)現(xiàn)對(duì)變量的遞增輸出。我們希望是兩次調(diào)用各自完成變量的遞增,相互之間不要有干擾,即兩次調(diào)用要求順序執(zhí)行。但事實(shí)上目前我們無(wú)法控制這種順序執(zhí)行(隨后會(huì)介紹synchronized關(guān)鍵字解決這個(gè)問(wèn)題)。所以結(jié)果是兩個(gè)線程交替執(zhí)行,確實(shí)實(shí)現(xiàn)了并發(fā),或者更抽象的說(shuō)兩個(gè)線程同時(shí)訪問(wèn)了某個(gè)資源,造成數(shù)據(jù)的不確定性。
9.5.2? synchronized關(guān)鍵字
在設(shè)計(jì)多線程模式中,解決線程沖突問(wèn)題都是采用synchronize關(guān)鍵字實(shí)現(xiàn)的。這意味著在給定時(shí)刻只允許一個(gè)線程訪問(wèn)共享資源。通常是在代碼前加上一條鎖語(yǔ)句實(shí)現(xiàn)的,這就保證了在一段時(shí)間內(nèi)只有一個(gè)線程運(yùn)行這段代碼,如果另一個(gè)線程需要訪問(wèn)這段共享資源,必須等待當(dāng)前的線程釋放鎖。可見(jiàn)鎖語(yǔ)句產(chǎn)生了一種互斥的效果,所以常常稱鎖為“互斥量”(mutex)。
要控制對(duì)共享資源的訪問(wèn),首先要把它封裝進(jìn)一個(gè)類(lèi),即編寫(xiě)一個(gè)方法來(lái)訪問(wèn)共享資源,為了保證對(duì)象在調(diào)用該方法訪問(wèn)資源時(shí)實(shí)現(xiàn)互斥訪問(wèn),必須提供保證機(jī)制,保證順序的訪問(wèn)共享資源。一般來(lái)說(shuō)類(lèi)中的數(shù)據(jù)成員都被聲明為私有的,只有通過(guò)方法來(lái)訪問(wèn)這些數(shù)據(jù)。所以可以把方法標(biāo)記為synchronized來(lái)防止資源沖突。
9.5.3? 同步控制方法
修改代碼9-4的部分代碼,使用synchronized關(guān)鍵字修飾方法,實(shí)現(xiàn)對(duì)方法的順序訪問(wèn),代碼修改部分如下:
11 public synchronized void printVal(int v,String y){
12? while(v<10)
13? System.out.println(y+":"+v++);
14 }
這里只是在方法前增加了一個(gè)關(guān)鍵字,這就表示如果一個(gè)線程調(diào)用該方方法,則必須首先獲得方法所在類(lèi)的對(duì)象的鎖,執(zhí)行完后釋放鎖。下一個(gè)線程在訪問(wèn)該方法前,先獲得鎖,然后再執(zhí)行代碼,這樣就實(shí)現(xiàn)了對(duì)共享資源(或關(guān)鍵代碼)的順序訪問(wèn)。保證了多線程的安全性。其執(zhí)行結(jié)果為:
9.5.4? 同步控制塊
實(shí)際中會(huì)遇到這樣一種情況,兩個(gè)函數(shù)共享公共資源,為了使資源得到保護(hù),必須實(shí)現(xiàn)資源訪問(wèn)地同步控制,尤其是 static方法和非static方法共享資源的情況更是如此。此時(shí)可以使用同步控制塊來(lái)解決這個(gè)問(wèn)題。代碼6同步控制塊示例顯示了具體用法。
代碼同步控制塊示例
1 class SynControlBlock implements Runnable{
2 private SomeObj obj
3 public static void method1(){
4? synchronized(obj){
5?? // 共享資源代碼
6? }
7 }
8 public void method2(){
9? synchronized(obj){
10?? // 共享資源代碼
11? }
12 }
13} 。
9.6? 線程的控制
線程是一個(gè)相對(duì)獨(dú)立的執(zhí)行單元,完成一個(gè)具體的任務(wù)。線程的可以被創(chuàng)建、執(zhí)行、阻塞、恢復(fù)執(zhí)行、結(jié)束等行為,這些行為組成了線程的控制機(jī)制。本節(jié)介紹線程控制的內(nèi)容、具體方法和實(shí)現(xiàn)方式。
9.6.1? 啟動(dòng)線程
無(wú)論是通過(guò)繼承Thread類(lèi)實(shí)現(xiàn)多線程還是通過(guò)實(shí)現(xiàn)Runnable接口實(shí)現(xiàn)多線程,如果要啟動(dòng)線程都需要 Thread類(lèi)的start()方法。該方法完成線程執(zhí)行的一些初始化工作。假設(shè)一個(gè)多線程類(lèi)繼承Thread實(shí)現(xiàn),類(lèi)名為MyThread,而另一個(gè)類(lèi)實(shí)現(xiàn)Runnable接口設(shè)計(jì)多線程,類(lèi)名為MyRunThread,則二者啟動(dòng)線程方式如下:
1 //創(chuàng)建類(lèi)MyThread的對(duì)象 thread,并啟動(dòng)該線程。
2 MyThread thread = new MyThread();
3? thread.start()
4 // 創(chuàng)建類(lèi)MyRunThread()的對(duì)象myRunThread,并啟動(dòng)該線程。
5 Thread myRunThread = new Thread(new MyRunThread());
6? myRunThread.start();
9.6.2? 掛起和恢復(fù)線程
在Java2之前,用戶會(huì)看到suspend()和resume()用來(lái)阻塞和喚醒線程,但是在 Java2中這兩個(gè)方法不再使用了。首先分析一下使用suspend()方法會(huì)發(fā)生什么問(wèn)題。suspend()方法的作用是掛起擁有鎖的線程,但是與 wait()方法不同,它不會(huì)釋放鎖。如果一線程調(diào)用suspend()方法,把另一個(gè)線程掛起,此時(shí)被掛起的線程在等待恢復(fù),而掛起它的線程在等待獲得鎖(該鎖就是被掛起的線程對(duì)象),此時(shí)就會(huì)發(fā)生死鎖。
9.6.3? 線程的休眠
Java提供了一種控制線程的方法Sleep(int miliseconds),這里稱為線程的休眠,它將線程停止一段時(shí)間,該時(shí)間由方法的參數(shù)決定,當(dāng)時(shí)間結(jié)束時(shí)線程進(jìn)入就緒狀態(tài),可以搶占CPU的時(shí)間周期。把例子代碼修改后觀察sleep()方法的作用,將得到代碼。
9.6.4? 等待和通知
等待和通知實(shí)現(xiàn)了線程之間的協(xié)調(diào)機(jī)制,使得線程之間可以建立“和諧”地協(xié)作關(guān)系。Java提供了線程對(duì)象的 wait()、notifty()或notifyAll()方法來(lái)實(shí)現(xiàn)這種協(xié)作,wait()方法使線程掛起一段時(shí)間,而notifty()或 notifyAll()方法使線程從wait()方法調(diào)用的狀態(tài)中恢復(fù)到就緒狀態(tài)。
Wait() 和sleep()方法相似,都是讓線程暫時(shí)掛起,都可以接受一個(gè)時(shí)間參數(shù),確定線程掛起時(shí)間。但是wait()方法有其特殊之處。
(1)線程一旦調(diào)用wait()方法,線程中同步方法的鎖被釋放,別的線程可以調(diào)用該線程中相應(yīng)的同步方法。
(2)使用wait()方法的線程可以使用 notifty()和notifyAll()方法獲得執(zhí)行的權(quán)利,即獲得搶占CPU周期的權(quán)利。
9.6.5? 結(jié)束線程
在Java2中stop()方法不再被支持,在將來(lái)的版本中該方法可能被替換但是由于其天生的不安全性,替換的方法也不會(huì)取得好的效果。盡管在Java2中不再支持該方法,但Java 仍然包含了該API,也就是說(shuō)程序員仍然可以調(diào)用該方法來(lái)結(jié)束線程。
但調(diào)用stop()方法終止一個(gè)線程時(shí),會(huì)釋放該線程持有的所有鎖,而問(wèn)題是用戶無(wú)法知道代碼目前的工作內(nèi)容,這是導(dǎo)致stop()方法不安全的因素。如果通過(guò)謹(jǐn)慎的設(shè)計(jì),或許可以實(shí)現(xiàn)安全的使用該方法。如果用戶知道在調(diào)用該方法時(shí),線程沒(méi)有處在處理或更新其他對(duì)象或數(shù)據(jù)的狀態(tài),則可以安全的使用該方法,但是有多少程序員會(huì)遇到這種情況呢,非常少。所以,多數(shù)情況下,用戶認(rèn)為自己安全的使用了stop()方法來(lái)結(jié)束線程,往往造成不可預(yù)知的后果。
9.7? 線程間通信
線程間進(jìn)行輸入輸出通信最常用的方式是“管道”方式。Java線程支持這種形式的通信。即一個(gè)線程從管道一端寫(xiě)入數(shù)據(jù),另一個(gè)線程從管道對(duì)端讀出數(shù)據(jù),用戶不必關(guān)心管道是如何傳輸數(shù)據(jù)和實(shí)現(xiàn)管道兩端的線程通信的。在Java的輸入輸出類(lèi)庫(kù)中兩個(gè)類(lèi) PipedWriter和PipedReader都支持管道通信方式。前者允許向管道寫(xiě)數(shù)據(jù),后者允許向不同的線程從同一個(gè)管道讀數(shù)據(jù)。下面分別介紹這連個(gè)類(lèi)和相應(yīng)的方法。
9.7.1? PipeWriter類(lèi)詳解
該類(lèi)的作用是創(chuàng)建一個(gè)PipedWriter類(lèi)對(duì)象writer,鏈接到一個(gè) PipedReader類(lèi)對(duì)象reader,從writer寫(xiě)入的數(shù)據(jù)從reader可以輕松讀出。該類(lèi)的聲明方式有兩種。
public PipedWriter(PipedReader reader) throws IOException{/*類(lèi)主體*/}
類(lèi)的構(gòu)造函數(shù)參數(shù)為 PipedReader類(lèi)對(duì)象,明確了建立管道鏈接的兩個(gè)對(duì)象。
public PipedWriter() throws IOException{/*類(lèi)主體*/}
9.7.2? 管道通信實(shí)例
代碼9-11說(shuō)明了線程間通過(guò)管道通信的實(shí)現(xiàn)方式。該程序建立兩個(gè)類(lèi),一個(gè)類(lèi)PipeSender負(fù)責(zé)向管道寫(xiě)數(shù)據(jù),一個(gè)類(lèi)PipedReceiver負(fù)責(zé)從管道讀數(shù)據(jù),但兩個(gè)線程都啟動(dòng)后,負(fù)責(zé)讀數(shù)據(jù)的線程不斷的從管道讀數(shù)據(jù),并打印到輸出屏幕上。
9.8? 多線程的死鎖問(wèn)題
上節(jié)講了線程的各種控制,以及如何避免資源的共享訪問(wèn)問(wèn)題。這些方法使讀者可以很方便的控制線程,但是正如一枚硬幣的兩面,它同時(shí)也帶來(lái)了不利的一面,即死鎖問(wèn)題。由于線程會(huì)進(jìn)入阻塞狀態(tài),并且對(duì)象同步鎖的存儲(chǔ)在,使得只有獲得對(duì)象的鎖才能訪問(wèn)該對(duì)象。因此很容易發(fā)上循環(huán)死鎖。如線程A等待線程B釋放鎖,而線程B等待線程C釋放鎖,線程C有等待線程A釋放鎖,這樣就造成一個(gè)輪回等待。三個(gè)線程都無(wú)法繼續(xù)運(yùn)行。
對(duì)于Java語(yǔ)言來(lái)講沒(méi)有很好地預(yù)防死鎖的方法,只有依靠讀者謹(jǐn)慎的設(shè)計(jì)來(lái)避免死鎖的發(fā)生。這里提供一個(gè)避免死鎖的基本原則。
(1)避免使用 suspend()和resume()方法,這些方法具有與生俱來(lái)產(chǎn)生死鎖的缺點(diǎn)。
(2)不要對(duì)長(zhǎng)時(shí)間I/O操作的方法施加鎖。
(3)使用多個(gè)鎖時(shí),確保所有線程都按相同的順序獲得鎖。
9.9? 多線程的缺點(diǎn)
多線程的主要目的是對(duì)大量并行的任務(wù)進(jìn)行有序地管理。通過(guò)同時(shí)執(zhí)行多個(gè)任務(wù),可以有效的利用計(jì)算機(jī)的資源(主要是提高CPU的利用率),或者實(shí)現(xiàn)對(duì)用戶來(lái)講響應(yīng)及時(shí)的程序界面。但是不可避免的任何“好東西”都有代價(jià),所以使用多線程也有其缺點(diǎn)主要包括:
(1)等待訪問(wèn)共享資源時(shí)使得程序運(yùn)行變慢。如果用戶訪問(wèn)網(wǎng)絡(luò)數(shù)據(jù)庫(kù),而改善數(shù)據(jù)庫(kù)的訪問(wèn)是互斥的,所以一個(gè)線程在訪問(wèn)大量數(shù)據(jù)或修改大量數(shù)據(jù)時(shí),其他線程就只用等待而不能執(zhí)行,同時(shí)如果把網(wǎng)絡(luò)鏈接和數(shù)據(jù)傳輸?shù)臅r(shí)間計(jì)算在內(nèi),則等待的時(shí)間或許是“不可忍受的”。
(2)當(dāng)線程數(shù)量增多時(shí),對(duì)線程的管理要求額外的CPU開(kāi)銷(xiāo)。雖然線程是輕量級(jí)進(jìn)程和其他線程共享一些數(shù)據(jù),但是畢竟每個(gè)線程需要自己的管理資源,而這些資源的管理會(huì)耗費(fèi)CPU時(shí)間片,如果線程數(shù)量增多到一定程度如(100個(gè)以上),則線程的管理開(kāi)銷(xiāo)代價(jià)會(huì)增大。
(3)死鎖是難以避免的,只有依靠程序員謹(jǐn)慎地設(shè)計(jì)多線程程序。任何語(yǔ)言都不可能提供預(yù)防死鎖的方法,Java也不例外除了盡量不使用控制線程的一些方法如suspend(),resume()外,需要認(rèn)真的分析線程的執(zhí)行過(guò)程,以避免線程間的死鎖。
(4)隨意使用線程技術(shù)有時(shí)會(huì)耗費(fèi)系統(tǒng)資源,所以要求程序員知道何時(shí)使用多線程以及何時(shí)避免使用該技術(shù)。
9.10? 習(xí)題
(1)簡(jiǎn)答題
1.解釋線程的概念,線程和進(jìn)程的區(qū)別是什么?
2.Java線程有幾中狀態(tài),各狀態(tài)之間轉(zhuǎn)換的條件?
3.創(chuàng)建線程有幾種途徑,這些途徑如何具體實(shí)現(xiàn)線程。它們的使用場(chǎng)合有什么不同,二者的關(guān)系?
4.Java如何定義線程的優(yōu)先級(jí),優(yōu)先級(jí)高的線程是否一定先于優(yōu)先級(jí)低的線程執(zhí)行,為什么?
5.使用Synchronized關(guān)鍵字可以實(shí)現(xiàn)鎖機(jī)制,實(shí)現(xiàn)資源的同步訪問(wèn),這里同步指什么?如何使用synchronized關(guān)鍵字?
6.線程等待和線程休眠都會(huì)使當(dāng)前線程停止執(zhí)行,而等待某個(gè)條件從而獲得繼續(xù)執(zhí)行的機(jī)會(huì),那么線程等待和線程休眠的區(qū)別在哪里?
7.線程間通信是通過(guò)管道流實(shí)現(xiàn)的,發(fā)送數(shù)據(jù)的線程把數(shù)據(jù)寫(xiě)入管道,接收數(shù)據(jù)的線程把數(shù)據(jù)從管道讀出,Java是如何通過(guò)管道流機(jī)制實(shí)現(xiàn)的線程間通信的?
8.解釋線程控制中方法resume()和suspend()方法的功能, 為什么這兩種控制線程的方法容易引起線程的死鎖?
9.如何避免線程的死鎖?
(2)編程題
1.繼承Thread類(lèi)編寫(xiě)一個(gè)線程類(lèi),覆寫(xiě)run()方法,每次啟動(dòng)線程時(shí)打印一行輸入說(shuō)明啟動(dòng)的是第幾個(gè)線程。
2.編寫(xiě)兩個(gè)線程類(lèi)(繼承自Thread),一個(gè)資源類(lèi)提供資源訪問(wèn)的方法,包括一個(gè)讀數(shù)據(jù)方法一個(gè)寫(xiě)數(shù)據(jù)方法,要求實(shí)現(xiàn)兩個(gè)線程對(duì)資源的互斥訪問(wèn)。
使用多線程最直接的例子是具有用戶界面的程序。如果用戶界面上設(shè)計(jì)了一個(gè)按鈕,一旦單擊該按鈕程序會(huì)自動(dòng)在網(wǎng)絡(luò)上搜索指定數(shù)據(jù),當(dāng)然這個(gè)過(guò)程會(huì)持續(xù)一段時(shí)間。如果沒(méi)有多線程實(shí)現(xiàn)技術(shù),就會(huì)出現(xiàn)用戶界面無(wú)法控制的局面,即在網(wǎng)絡(luò)數(shù)據(jù)搜索完之前,用戶界面根本不響應(yīng)其他界面輸入。整個(gè)界面象是靜止在那里而無(wú)法操作。而我們希望不管系統(tǒng)當(dāng)前在完成什么任務(wù),都允許用戶操作界面元素,如查詢數(shù)據(jù),完成其他信息的處理等。這樣就要求程序可以同時(shí)執(zhí)行多個(gè)任務(wù),響應(yīng)用戶的不同操作請(qǐng)求。對(duì)于用戶而言就仿佛有多個(gè)處理器在為其工作。而在單處理器的計(jì)算機(jī)上完成程序的多任務(wù)功能就需要多線程技術(shù)。
多線程技術(shù)可以模擬多處理器的效果,對(duì)用戶而言計(jì)算機(jī)同時(shí)完成一個(gè)程序的多個(gè)任務(wù),而實(shí)際上該機(jī)制使得計(jì)算機(jī)把CPU周期按照一定策略分配給每一個(gè)線程,而高速的CPU使得用戶覺(jué)得計(jì)算機(jī)在同時(shí)完成多個(gè)任務(wù)。
9.1? 線程概述
線程是操作系統(tǒng)的概念,線程也稱之為輕量級(jí)進(jìn)程(lightweight process LWP),是CPU的基本使用單元,它的輕量級(jí)名稱是和進(jìn)程相關(guān)的。線程由線程ID、程序記數(shù)器、寄存器和堆棧組成,多個(gè)線程可以共享代碼段、數(shù)據(jù)段和諸如打開(kāi)的文件等的系統(tǒng)資源。而傳統(tǒng)的進(jìn)程其實(shí)就是單線程控制程序,每個(gè)進(jìn)程都有自己的代碼段、數(shù)據(jù)段和其他系統(tǒng)資源。這無(wú)疑使得每個(gè)進(jìn)程管理更多的內(nèi)容,從而稱為重量級(jí)進(jìn)程。“輕量”是指線程沒(méi)有獨(dú)自的存儲(chǔ)空間,和同一個(gè)進(jìn)程的多個(gè)線程共享存儲(chǔ)空間。
多線程和傳統(tǒng)的單線程在程序設(shè)計(jì)上的最大區(qū)別是每個(gè)線程獨(dú)自運(yùn)行,是彼此獨(dú)立的指令流,造成線程之間的執(zhí)行是亂序的,所以線程的控制需要謹(jǐn)慎對(duì)待。
下面分別詳細(xì)介紹進(jìn)程和線程的概念,如何創(chuàng)建線程、設(shè)置線程的優(yōu)先級(jí)、線程控制和線程同步等關(guān)鍵問(wèn)題。
9.2? 創(chuàng)建線程
在學(xué)習(xí)線程前,一定要先了解Java的線程機(jī)制,然后學(xué)習(xí)如何利用Thread類(lèi)實(shí)現(xiàn)多線程。Java的多線程機(jī)制提供了兩種方式實(shí)現(xiàn)多線程編程,一種是通過(guò)繼承java.long.Thread類(lèi)來(lái)實(shí)現(xiàn),一種是通過(guò)實(shí)現(xiàn)Runnable接口實(shí)現(xiàn)。
9.2.1? 繼承Thread類(lèi)創(chuàng)建線程
Thread類(lèi)是Java實(shí)現(xiàn)多線程的提供了簡(jiǎn)單的方法,Thread類(lèi)已經(jīng)具備了運(yùn)行多線程所需要的資源,用戶只需要重載該類(lèi)的run()方法,把需要使用多線程運(yùn)行的代碼放入該方法。這樣這些代碼就可以和其他線程“同時(shí)”存在。創(chuàng)建線程對(duì)象并用該對(duì)象調(diào)用start()方法則線程開(kāi)始運(yùn)行,start()方法提供了啟動(dòng)線程和線程運(yùn)行所需要的框架。
代碼是一個(gè)例子,說(shuō)明使用繼承 Thread類(lèi)實(shí)現(xiàn)多線程。每次new一個(gè)線程都設(shè)置一個(gè)線程計(jì)數(shù)器,表明建立的線程數(shù)。整個(gè)程序啟動(dòng)3個(gè)線程,每個(gè)線程會(huì)有9次輸出,但是三個(gè)線程的建立并非順序執(zhí)行,而每個(gè)線程的9次輸出也不一定會(huì)順序輸出。如代碼繼承Thread類(lèi)實(shí)現(xiàn)多線程示例所示。
9.2.2? 實(shí)現(xiàn)Runnable接口創(chuàng)建線程
Java提供了另一個(gè)有用的接口實(shí)現(xiàn)多線程編程。因?yàn)镴ava不支持多繼承,所以如果用戶的類(lèi)已經(jīng)繼承了一個(gè)類(lèi),而又需要多線程機(jī)制的支持,此時(shí)繼承thread類(lèi)就不現(xiàn)實(shí)了。所以Runnable接口在這種情況下就很實(shí)用。
Runnable 接口有唯一一個(gè)方法run(),所以實(shí)現(xiàn)該接口時(shí)必須自己定義該方法,提供多線程需要執(zhí)行的代碼。如果運(yùn)行通過(guò)實(shí)現(xiàn)Runnable接口的多線程程序,則需要借助Thread類(lèi),因?yàn)镽unnable接口沒(méi)有提供任何東西支持多線程,必須借助Thead類(lèi)的框架實(shí)現(xiàn)多線程,即通過(guò)類(lèi)Thread的構(gòu)造函數(shù) public Thread(Runnable target)來(lái)實(shí)現(xiàn)。代碼是通過(guò)繼承Runnable接口而實(shí)現(xiàn)多線程的例子。我們分析和運(yùn)行該程序,觀察輸出結(jié)果就可以很好的理解其運(yùn)用。
9.3? 線程的狀態(tài)
在Java中線程的執(zhí)行過(guò)程稍微有些復(fù)雜,但線程對(duì)象創(chuàng)建后并不不是立即執(zhí)行,需要做些準(zhǔn)備工作才有執(zhí)行的權(quán)利,而一旦搶占到CPU周期,則線程可以運(yùn)行,但CPU周期結(jié)束則線程必須暫時(shí)停止,或線程執(zhí)行過(guò)程中的某個(gè)條件無(wú)法滿足時(shí)也會(huì)暫時(shí)停止,只有等待條件滿足時(shí)才會(huì)繼續(xù)執(zhí)行,最后從run()方法返回后,線程退出。可以看出線程的執(zhí)行過(guò)程中涉及一些狀態(tài),線程就在這些狀態(tài)之間遷移。
做一點(diǎn)說(shuō)明,Java 規(guī)范中只定義了線程的四種狀態(tài),即新建狀態(tài)、可運(yùn)行狀態(tài)、阻塞狀態(tài)和死亡狀態(tài)。為了更清晰的說(shuō)明線程的狀態(tài)變化過(guò)程,我們認(rèn)為劃分為五個(gè)狀態(tài)更好理解,這里把可運(yùn)行狀態(tài)(Runnable)分解為就緒狀態(tài)和運(yùn)行狀態(tài),可以更好的理解可運(yùn)行狀態(tài)的含義。
線程包括五個(gè)狀態(tài):新建狀態(tài)、就緒狀態(tài)、運(yùn)行狀態(tài)、阻塞狀態(tài)和死亡狀態(tài)。下面分別詳細(xì)介紹這五種狀態(tài)。
9.4? 線程的優(yōu)先級(jí)
線程的有限級(jí)表示一個(gè)線程被CPU執(zhí)行的機(jī)會(huì)多少。注意,這里用“機(jī)會(huì)多少”來(lái)表達(dá)而不是用“先后順序”來(lái)表達(dá)。在Java中雖然定義了設(shè)置線程優(yōu)先級(jí)高低的方法,但是優(yōu)先級(jí)低并不意味著在不同優(yōu)先級(jí)的線程中就不會(huì)被執(zhí)行,優(yōu)先級(jí)低只說(shuō)明該線程被執(zhí)行的概率小,同理優(yōu)先級(jí)高的線程獲得CPU的概率就大。
通過(guò)Tread類(lèi)的setPriority()方法設(shè)置線程的優(yōu)先級(jí),該方法的參數(shù)為int型,其實(shí) Java提供了三個(gè)優(yōu)先級(jí)別,都為T(mén)hread類(lèi)的常量,從高到低依次為T(mén)hread. MAX-PRIORITY、Thread.NORM_PRIORITY、Thread.MIN_PRIORITY。這里再次重申,優(yōu)先級(jí)低并不意味著線程得不到執(zhí)行,而是線程被優(yōu)先執(zhí)行的概率小。這也說(shuō)明設(shè)置線程的優(yōu)先級(jí)不會(huì)造成死鎖的發(fā)生。
9.5? 線程的同步
在多線程中經(jīng)常遇到的一個(gè)問(wèn)題就是資源共享問(wèn)題,假設(shè)兩個(gè)線程同時(shí)訪問(wèn)一個(gè)數(shù)據(jù)區(qū),一個(gè)讀數(shù)據(jù)、一個(gè)寫(xiě)數(shù)據(jù),在一個(gè)線程讀數(shù)據(jù)前另一個(gè)線程修改了數(shù)據(jù),則讀數(shù)據(jù)線程讀到的不是原始數(shù)據(jù)而是被修改過(guò)的數(shù)據(jù),顯然這樣使不允許的。而在多線程編程中經(jīng)常會(huì)遇到訪問(wèn)共享資源的問(wèn)題,這些資源可以是數(shù)據(jù)、文件、一塊內(nèi)存區(qū)或是外圍設(shè)備的訪問(wèn)等。所以必須解決多線程編程中如何實(shí)現(xiàn)資源的共享問(wèn)題,在Java中稱為線程的同步問(wèn)題,在多數(shù)的編程語(yǔ)言中解決共享資源沖突的方法是采用順序機(jī)制(Serialize),通過(guò)為共享資源加鎖的方法實(shí)現(xiàn)資源的順序訪問(wèn)。
9.5.1? Java程序的資源共享
通過(guò)下面的例子,說(shuō)明如果沒(méi)有實(shí)現(xiàn)線程同步的訪問(wèn)共享資源會(huì)遇到的問(wèn)題。我們?cè)O(shè)計(jì)一個(gè)線程類(lèi) FooOne,該類(lèi)的多線程代碼無(wú)限循環(huán)的輸出一個(gè)值,每次循環(huán)該值遞增,但遞增到100時(shí),停止循環(huán)線程退出,這由方法run()中調(diào)用方法 printVal()實(shí)現(xiàn)。另一個(gè)線程類(lèi)FooTwo調(diào)用類(lèi)FooOne的對(duì)象,該類(lèi)的run()方法調(diào)用類(lèi)FooOne對(duì)象的printVal()方法,也實(shí)現(xiàn)對(duì)變量的遞增輸出。我們希望是兩次調(diào)用各自完成變量的遞增,相互之間不要有干擾,即兩次調(diào)用要求順序執(zhí)行。但事實(shí)上目前我們無(wú)法控制這種順序執(zhí)行(隨后會(huì)介紹synchronized關(guān)鍵字解決這個(gè)問(wèn)題)。所以結(jié)果是兩個(gè)線程交替執(zhí)行,確實(shí)實(shí)現(xiàn)了并發(fā),或者更抽象的說(shuō)兩個(gè)線程同時(shí)訪問(wèn)了某個(gè)資源,造成數(shù)據(jù)的不確定性。
9.5.2? synchronized關(guān)鍵字
在設(shè)計(jì)多線程模式中,解決線程沖突問(wèn)題都是采用synchronize關(guān)鍵字實(shí)現(xiàn)的。這意味著在給定時(shí)刻只允許一個(gè)線程訪問(wèn)共享資源。通常是在代碼前加上一條鎖語(yǔ)句實(shí)現(xiàn)的,這就保證了在一段時(shí)間內(nèi)只有一個(gè)線程運(yùn)行這段代碼,如果另一個(gè)線程需要訪問(wèn)這段共享資源,必須等待當(dāng)前的線程釋放鎖。可見(jiàn)鎖語(yǔ)句產(chǎn)生了一種互斥的效果,所以常常稱鎖為“互斥量”(mutex)。
要控制對(duì)共享資源的訪問(wèn),首先要把它封裝進(jìn)一個(gè)類(lèi),即編寫(xiě)一個(gè)方法來(lái)訪問(wèn)共享資源,為了保證對(duì)象在調(diào)用該方法訪問(wèn)資源時(shí)實(shí)現(xiàn)互斥訪問(wèn),必須提供保證機(jī)制,保證順序的訪問(wèn)共享資源。一般來(lái)說(shuō)類(lèi)中的數(shù)據(jù)成員都被聲明為私有的,只有通過(guò)方法來(lái)訪問(wèn)這些數(shù)據(jù)。所以可以把方法標(biāo)記為synchronized來(lái)防止資源沖突。
9.5.3? 同步控制方法
修改代碼9-4的部分代碼,使用synchronized關(guān)鍵字修飾方法,實(shí)現(xiàn)對(duì)方法的順序訪問(wèn),代碼修改部分如下:
11 public synchronized void printVal(int v,String y){
12? while(v<10)
13? System.out.println(y+":"+v++);
14 }
這里只是在方法前增加了一個(gè)關(guān)鍵字,這就表示如果一個(gè)線程調(diào)用該方方法,則必須首先獲得方法所在類(lèi)的對(duì)象的鎖,執(zhí)行完后釋放鎖。下一個(gè)線程在訪問(wèn)該方法前,先獲得鎖,然后再執(zhí)行代碼,這樣就實(shí)現(xiàn)了對(duì)共享資源(或關(guān)鍵代碼)的順序訪問(wèn)。保證了多線程的安全性。其執(zhí)行結(jié)果為:
9.5.4? 同步控制塊
實(shí)際中會(huì)遇到這樣一種情況,兩個(gè)函數(shù)共享公共資源,為了使資源得到保護(hù),必須實(shí)現(xiàn)資源訪問(wèn)地同步控制,尤其是 static方法和非static方法共享資源的情況更是如此。此時(shí)可以使用同步控制塊來(lái)解決這個(gè)問(wèn)題。代碼6同步控制塊示例顯示了具體用法。
代碼同步控制塊示例
1 class SynControlBlock implements Runnable{
2 private SomeObj obj
3 public static void method1(){
4? synchronized(obj){
5?? // 共享資源代碼
6? }
7 }
8 public void method2(){
9? synchronized(obj){
10?? // 共享資源代碼
11? }
12 }
13} 。
9.6? 線程的控制
線程是一個(gè)相對(duì)獨(dú)立的執(zhí)行單元,完成一個(gè)具體的任務(wù)。線程的可以被創(chuàng)建、執(zhí)行、阻塞、恢復(fù)執(zhí)行、結(jié)束等行為,這些行為組成了線程的控制機(jī)制。本節(jié)介紹線程控制的內(nèi)容、具體方法和實(shí)現(xiàn)方式。
9.6.1? 啟動(dòng)線程
無(wú)論是通過(guò)繼承Thread類(lèi)實(shí)現(xiàn)多線程還是通過(guò)實(shí)現(xiàn)Runnable接口實(shí)現(xiàn)多線程,如果要啟動(dòng)線程都需要 Thread類(lèi)的start()方法。該方法完成線程執(zhí)行的一些初始化工作。假設(shè)一個(gè)多線程類(lèi)繼承Thread實(shí)現(xiàn),類(lèi)名為MyThread,而另一個(gè)類(lèi)實(shí)現(xiàn)Runnable接口設(shè)計(jì)多線程,類(lèi)名為MyRunThread,則二者啟動(dòng)線程方式如下:
1 //創(chuàng)建類(lèi)MyThread的對(duì)象 thread,并啟動(dòng)該線程。
2 MyThread thread = new MyThread();
3? thread.start()
4 // 創(chuàng)建類(lèi)MyRunThread()的對(duì)象myRunThread,并啟動(dòng)該線程。
5 Thread myRunThread = new Thread(new MyRunThread());
6? myRunThread.start();
9.6.2? 掛起和恢復(fù)線程
在Java2之前,用戶會(huì)看到suspend()和resume()用來(lái)阻塞和喚醒線程,但是在 Java2中這兩個(gè)方法不再使用了。首先分析一下使用suspend()方法會(huì)發(fā)生什么問(wèn)題。suspend()方法的作用是掛起擁有鎖的線程,但是與 wait()方法不同,它不會(huì)釋放鎖。如果一線程調(diào)用suspend()方法,把另一個(gè)線程掛起,此時(shí)被掛起的線程在等待恢復(fù),而掛起它的線程在等待獲得鎖(該鎖就是被掛起的線程對(duì)象),此時(shí)就會(huì)發(fā)生死鎖。
9.6.3? 線程的休眠
Java提供了一種控制線程的方法Sleep(int miliseconds),這里稱為線程的休眠,它將線程停止一段時(shí)間,該時(shí)間由方法的參數(shù)決定,當(dāng)時(shí)間結(jié)束時(shí)線程進(jìn)入就緒狀態(tài),可以搶占CPU的時(shí)間周期。把例子代碼修改后觀察sleep()方法的作用,將得到代碼。
9.6.4? 等待和通知
等待和通知實(shí)現(xiàn)了線程之間的協(xié)調(diào)機(jī)制,使得線程之間可以建立“和諧”地協(xié)作關(guān)系。Java提供了線程對(duì)象的 wait()、notifty()或notifyAll()方法來(lái)實(shí)現(xiàn)這種協(xié)作,wait()方法使線程掛起一段時(shí)間,而notifty()或 notifyAll()方法使線程從wait()方法調(diào)用的狀態(tài)中恢復(fù)到就緒狀態(tài)。
Wait() 和sleep()方法相似,都是讓線程暫時(shí)掛起,都可以接受一個(gè)時(shí)間參數(shù),確定線程掛起時(shí)間。但是wait()方法有其特殊之處。
(1)線程一旦調(diào)用wait()方法,線程中同步方法的鎖被釋放,別的線程可以調(diào)用該線程中相應(yīng)的同步方法。
(2)使用wait()方法的線程可以使用 notifty()和notifyAll()方法獲得執(zhí)行的權(quán)利,即獲得搶占CPU周期的權(quán)利。
9.6.5? 結(jié)束線程
在Java2中stop()方法不再被支持,在將來(lái)的版本中該方法可能被替換但是由于其天生的不安全性,替換的方法也不會(huì)取得好的效果。盡管在Java2中不再支持該方法,但Java 仍然包含了該API,也就是說(shuō)程序員仍然可以調(diào)用該方法來(lái)結(jié)束線程。
但調(diào)用stop()方法終止一個(gè)線程時(shí),會(huì)釋放該線程持有的所有鎖,而問(wèn)題是用戶無(wú)法知道代碼目前的工作內(nèi)容,這是導(dǎo)致stop()方法不安全的因素。如果通過(guò)謹(jǐn)慎的設(shè)計(jì),或許可以實(shí)現(xiàn)安全的使用該方法。如果用戶知道在調(diào)用該方法時(shí),線程沒(méi)有處在處理或更新其他對(duì)象或數(shù)據(jù)的狀態(tài),則可以安全的使用該方法,但是有多少程序員會(huì)遇到這種情況呢,非常少。所以,多數(shù)情況下,用戶認(rèn)為自己安全的使用了stop()方法來(lái)結(jié)束線程,往往造成不可預(yù)知的后果。
9.7? 線程間通信
線程間進(jìn)行輸入輸出通信最常用的方式是“管道”方式。Java線程支持這種形式的通信。即一個(gè)線程從管道一端寫(xiě)入數(shù)據(jù),另一個(gè)線程從管道對(duì)端讀出數(shù)據(jù),用戶不必關(guān)心管道是如何傳輸數(shù)據(jù)和實(shí)現(xiàn)管道兩端的線程通信的。在Java的輸入輸出類(lèi)庫(kù)中兩個(gè)類(lèi) PipedWriter和PipedReader都支持管道通信方式。前者允許向管道寫(xiě)數(shù)據(jù),后者允許向不同的線程從同一個(gè)管道讀數(shù)據(jù)。下面分別介紹這連個(gè)類(lèi)和相應(yīng)的方法。
9.7.1? PipeWriter類(lèi)詳解
該類(lèi)的作用是創(chuàng)建一個(gè)PipedWriter類(lèi)對(duì)象writer,鏈接到一個(gè) PipedReader類(lèi)對(duì)象reader,從writer寫(xiě)入的數(shù)據(jù)從reader可以輕松讀出。該類(lèi)的聲明方式有兩種。
public PipedWriter(PipedReader reader) throws IOException{/*類(lèi)主體*/}
類(lèi)的構(gòu)造函數(shù)參數(shù)為 PipedReader類(lèi)對(duì)象,明確了建立管道鏈接的兩個(gè)對(duì)象。
public PipedWriter() throws IOException{/*類(lèi)主體*/}
9.7.2? 管道通信實(shí)例
代碼9-11說(shuō)明了線程間通過(guò)管道通信的實(shí)現(xiàn)方式。該程序建立兩個(gè)類(lèi),一個(gè)類(lèi)PipeSender負(fù)責(zé)向管道寫(xiě)數(shù)據(jù),一個(gè)類(lèi)PipedReceiver負(fù)責(zé)從管道讀數(shù)據(jù),但兩個(gè)線程都啟動(dòng)后,負(fù)責(zé)讀數(shù)據(jù)的線程不斷的從管道讀數(shù)據(jù),并打印到輸出屏幕上。
9.8? 多線程的死鎖問(wèn)題
上節(jié)講了線程的各種控制,以及如何避免資源的共享訪問(wèn)問(wèn)題。這些方法使讀者可以很方便的控制線程,但是正如一枚硬幣的兩面,它同時(shí)也帶來(lái)了不利的一面,即死鎖問(wèn)題。由于線程會(huì)進(jìn)入阻塞狀態(tài),并且對(duì)象同步鎖的存儲(chǔ)在,使得只有獲得對(duì)象的鎖才能訪問(wèn)該對(duì)象。因此很容易發(fā)上循環(huán)死鎖。如線程A等待線程B釋放鎖,而線程B等待線程C釋放鎖,線程C有等待線程A釋放鎖,這樣就造成一個(gè)輪回等待。三個(gè)線程都無(wú)法繼續(xù)運(yùn)行。
對(duì)于Java語(yǔ)言來(lái)講沒(méi)有很好地預(yù)防死鎖的方法,只有依靠讀者謹(jǐn)慎的設(shè)計(jì)來(lái)避免死鎖的發(fā)生。這里提供一個(gè)避免死鎖的基本原則。
(1)避免使用 suspend()和resume()方法,這些方法具有與生俱來(lái)產(chǎn)生死鎖的缺點(diǎn)。
(2)不要對(duì)長(zhǎng)時(shí)間I/O操作的方法施加鎖。
(3)使用多個(gè)鎖時(shí),確保所有線程都按相同的順序獲得鎖。
9.9? 多線程的缺點(diǎn)
多線程的主要目的是對(duì)大量并行的任務(wù)進(jìn)行有序地管理。通過(guò)同時(shí)執(zhí)行多個(gè)任務(wù),可以有效的利用計(jì)算機(jī)的資源(主要是提高CPU的利用率),或者實(shí)現(xiàn)對(duì)用戶來(lái)講響應(yīng)及時(shí)的程序界面。但是不可避免的任何“好東西”都有代價(jià),所以使用多線程也有其缺點(diǎn)主要包括:
(1)等待訪問(wèn)共享資源時(shí)使得程序運(yùn)行變慢。如果用戶訪問(wèn)網(wǎng)絡(luò)數(shù)據(jù)庫(kù),而改善數(shù)據(jù)庫(kù)的訪問(wèn)是互斥的,所以一個(gè)線程在訪問(wèn)大量數(shù)據(jù)或修改大量數(shù)據(jù)時(shí),其他線程就只用等待而不能執(zhí)行,同時(shí)如果把網(wǎng)絡(luò)鏈接和數(shù)據(jù)傳輸?shù)臅r(shí)間計(jì)算在內(nèi),則等待的時(shí)間或許是“不可忍受的”。
(2)當(dāng)線程數(shù)量增多時(shí),對(duì)線程的管理要求額外的CPU開(kāi)銷(xiāo)。雖然線程是輕量級(jí)進(jìn)程和其他線程共享一些數(shù)據(jù),但是畢竟每個(gè)線程需要自己的管理資源,而這些資源的管理會(huì)耗費(fèi)CPU時(shí)間片,如果線程數(shù)量增多到一定程度如(100個(gè)以上),則線程的管理開(kāi)銷(xiāo)代價(jià)會(huì)增大。
(3)死鎖是難以避免的,只有依靠程序員謹(jǐn)慎地設(shè)計(jì)多線程程序。任何語(yǔ)言都不可能提供預(yù)防死鎖的方法,Java也不例外除了盡量不使用控制線程的一些方法如suspend(),resume()外,需要認(rèn)真的分析線程的執(zhí)行過(guò)程,以避免線程間的死鎖。
(4)隨意使用線程技術(shù)有時(shí)會(huì)耗費(fèi)系統(tǒng)資源,所以要求程序員知道何時(shí)使用多線程以及何時(shí)避免使用該技術(shù)。
9.10? 習(xí)題
(1)簡(jiǎn)答題
1.解釋線程的概念,線程和進(jìn)程的區(qū)別是什么?
2.Java線程有幾中狀態(tài),各狀態(tài)之間轉(zhuǎn)換的條件?
3.創(chuàng)建線程有幾種途徑,這些途徑如何具體實(shí)現(xiàn)線程。它們的使用場(chǎng)合有什么不同,二者的關(guān)系?
4.Java如何定義線程的優(yōu)先級(jí),優(yōu)先級(jí)高的線程是否一定先于優(yōu)先級(jí)低的線程執(zhí)行,為什么?
5.使用Synchronized關(guān)鍵字可以實(shí)現(xiàn)鎖機(jī)制,實(shí)現(xiàn)資源的同步訪問(wèn),這里同步指什么?如何使用synchronized關(guān)鍵字?
6.線程等待和線程休眠都會(huì)使當(dāng)前線程停止執(zhí)行,而等待某個(gè)條件從而獲得繼續(xù)執(zhí)行的機(jī)會(huì),那么線程等待和線程休眠的區(qū)別在哪里?
7.線程間通信是通過(guò)管道流實(shí)現(xiàn)的,發(fā)送數(shù)據(jù)的線程把數(shù)據(jù)寫(xiě)入管道,接收數(shù)據(jù)的線程把數(shù)據(jù)從管道讀出,Java是如何通過(guò)管道流機(jī)制實(shí)現(xiàn)的線程間通信的?
8.解釋線程控制中方法resume()和suspend()方法的功能, 為什么這兩種控制線程的方法容易引起線程的死鎖?
9.如何避免線程的死鎖?
(2)編程題
1.繼承Thread類(lèi)編寫(xiě)一個(gè)線程類(lèi),覆寫(xiě)run()方法,每次啟動(dòng)線程時(shí)打印一行輸入說(shuō)明啟動(dòng)的是第幾個(gè)線程。
2.編寫(xiě)兩個(gè)線程類(lèi)(繼承自Thread),一個(gè)資源類(lèi)提供資源訪問(wèn)的方法,包括一個(gè)讀數(shù)據(jù)方法一個(gè)寫(xiě)數(shù)據(jù)方法,要求實(shí)現(xiàn)兩個(gè)線程對(duì)資源的互斥訪問(wèn)。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫(xiě)作最大的動(dòng)力,如果您喜歡我的文章,感覺(jué)我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
