[摘 要]本文就如何實現(xiàn)Java 的多線程、線程調(diào)度模式、同步互斥機制以及內(nèi)置多線程功能進行了深入的探討,并對線程的狀態(tài)、創(chuàng)建和控制方法以及避免死鎖的方法作了歸納總結(jié),指出了線程實際應用領域以及在編程時應注意的事項。
[關鍵詞]多線程 線程調(diào)度 同步機制 死鎖
作者簡介:聞麗華(1969-),女,大連水產(chǎn)學院職業(yè)技術學院 計算機系講師 工程碩士學位。
引入線程是十分必要的,線程共享相同的地址空間并共同構(gòu)成一個大的進程,所以同一進程中的線程間的通訊是非常簡單而有效的,上下文切換非常快并且是整個大程序的一部分切換。線程僅是過程調(diào)用,它們彼此獨立執(zhí)行,線程使得在一個應用程序中同時使用多個線程來完成不同的任務,程序的編寫更加自由和豐富,可以大大簡化應用程序設計。如果要一程序中實現(xiàn)多段代碼同時交替運行,就需要產(chǎn)生多個線程,并指定每個線程上所要運行的程序代碼段,這就是多線程。多線程可以增進程序的交互性, 提供更好功能、更好的GUI 和更好的服務器功能。
Java 在兩方面支持多線程,一方面,Java 環(huán)境本身就是多線程的,若干個系統(tǒng)線程運行負責必要的無用單元回收, 系統(tǒng)維護等系統(tǒng)級操作;另一方面,Java 語言內(nèi)置多線程控制, 可以大大簡化多線程應用程序開發(fā)。
一、Java 中如何實現(xiàn)多線程
Java 的多線程機制使得在一個程序里可同時執(zhí)行多個任務。 只有在多CPU 的計算機或者在網(wǎng)絡計算體系結(jié)構(gòu)下,將Java 程序劃分為多個并發(fā)執(zhí)行線程后,同時啟動多個線程運行,使不同的線程運行在基于不同處理器的Java虛擬機中,才能提高應用程序的執(zhí)行效率。
(一)Java多線程實現(xiàn)方法
Java 采用兩種途徑實現(xiàn)多線程機制:一種是應用程序的并發(fā)運行對象直接繼承Thread 類,另外一種是定義并發(fā)執(zhí)行對象實現(xiàn)Runnable 接口。
1.用Thread 類創(chuàng)建線程
Java的線程是通過java.lang.Thread類來控制的,一個Thread類的對象代表一個線程。當編寫Thread 類的子類時,在子類中重寫父類的run()方法,該方法中包含了線程的操作,這樣的程序需要建立自己的線程時,只需要創(chuàng)建一個已定義好的Thread 子類的實例就可以了,當創(chuàng)建的線程需要調(diào)用start()方法開始運行時,run()方法將被自動執(zhí)行。
2.使用Runnable 接口創(chuàng)建多線程
通過實現(xiàn)Runnable 作為一個目標對象,用Runnable 目標對象初始化Thread 類,提供run()方法,實現(xiàn)的同時還可以繼承其他類,可以避免由單繼承的局限。幾乎所有的線程都可以用Runnable 接口。當線程被構(gòu)造時, 需要的代碼和數(shù)據(jù)通過一個對象作為構(gòu)造函數(shù)實參傳遞進去, 這個對象就是實現(xiàn)了Runnable接口的類的實例。
3.兩種方法的對比分析
直接繼承Thread 類的方法不能再從其他類繼承,編寫簡單,可以直接操作線程;實現(xiàn)Runnable 接口的方法適合多個相同程序代碼的線程去處理同一資源的情況,可以避免由于Java單繼承特性帶來的局限。兩者的重要區(qū)別在于啟動多線程對象的設計方法不同。在具體應用中,采用哪種方法來構(gòu)造線程要視情況而定。事實上,幾乎所有多線程應用都可用第二種方式,即實現(xiàn)Runnable接口。
(二)線程的狀態(tài)控制
線程包括四個狀態(tài):new (開始),running (運行),wait (等候)和done(結(jié)束)。當線程被創(chuàng)建并還未運行時, 線程處于new 狀態(tài), 在這個狀態(tài)下,線程不能運行。對于新創(chuàng)建的線程,調(diào)用start 方法之后, 會自動調(diào)用start 方法,這時線程進入就緒狀態(tài)。在程序之間用某種方法把處理器的執(zhí)行時間分成時間片, 位于就緒狀態(tài)的每個線程都是能運行的,但在某一時刻, 系統(tǒng)處理器只能運行一個線程。由于某些原因, 線程可以被臨時暫停進入等候狀態(tài)。處于這種狀態(tài)的線程,對于用戶來講仍然有效,仍然可以重新進入就緒狀態(tài)。當線程因不再需要而進入結(jié)束狀態(tài)時,線程就不能再被恢復和執(zhí)行。
(三)線程的調(diào)度機制
為了控制線程的運行,Java定義了線程調(diào)度器來監(jiān)控系統(tǒng)中處于就緒狀態(tài)的所有線程。 線程調(diào)度器按照線程的優(yōu)先級決定那個線程投入處理器運行,在多個線程處于就緒狀態(tài)的條件下,具有高優(yōu)先級的線程會在低優(yōu)先級線程之前得到執(zhí)行,線程調(diào)度器同樣采用“搶占式”策略來調(diào)度線程執(zhí)行,即當前線程執(zhí)行過程中有較高優(yōu)先級的線程進入就緒狀態(tài),則高優(yōu)先級的線程立即被調(diào)度執(zhí)行,具有相同優(yōu)先級的所有線程采用輪轉(zhuǎn)的方式來共同分配CPU 時間片。
線程調(diào)度的意義在于避免多個線程爭用有限資源而導致應用系統(tǒng)死機或者崩潰。Java 支持搶占式調(diào)度,因此線程的優(yōu)先級尤為重要,它是線程調(diào)度的決策依據(jù)。Java 中線程的優(yōu)先級分為10 個等級,分別用1~10 之間的數(shù)字表示,數(shù)字越大表明線程的級別越高,線程創(chuàng)建時,子線程繼承父線程的優(yōu)先級。線程運行的順序以及從處理器中獲得的時間數(shù)量主要取決于開發(fā)者, 處理器給每個線程分配一個時間片, 而且線程的運行不能影響整個系統(tǒng)。處理器線程的系統(tǒng)或者是搶占式的, 或者是非搶占式的。搶占式系統(tǒng)在任何給定的時間內(nèi)將運行最高優(yōu)先級的線程, 系統(tǒng)中的所有線程都有自己的優(yōu)先級。Thread 類提供了setPriority 和getPriority方法來重新設置和讀取優(yōu)先權。Java 虛擬機是搶占式的, 它能保證運行優(yōu)先級最高的線程。
Java 通過創(chuàng)建了線程組管理成百上千個線程。線程組是線程的一個譜系組, 每個組包含的線程數(shù)不受限制,能對每個線程命名并能在整個線程組中執(zhí)行(Suspend)和停止(Stop)這樣的操作。
(四)多線程的同步機制
Java 應用程序的多個線程共享同一進程的數(shù)據(jù)資源,多個用戶線程在并發(fā)運行過程中可能同時訪問具有敏感性的內(nèi)容,競爭共享資源。必須采用某種方法來確定資源在某一時刻僅被一個線程占用,達到此目的的過程叫同步(synchronized),在Java 中定義了線程同步的概念,實現(xiàn)對共享資源的一致性維護。多線程同步機制的實現(xiàn)是基于管程 (Monitor)機制。 管程是一個互斥獨占鎖定的對象,或稱互斥體。 在給定的時間里,僅有一個線程可以獲得管程。所有其他試圖進入已經(jīng)鎖定的管程的線程必須掛起,當擁有管程的線程從同步方法中返回或異常退出時,其他線程才可以獲得管程。可以用synchronized 和Object 類的方法wait ()、notify()和notifyAll()實現(xiàn)多線程同步。 線程同步有同步方法和同步語句兩種: (1)同步方法。 在方法的聲明中用synchronized 關鍵字修飾,可以對該方法中的所有代碼實現(xiàn)同步,其格式為public synchronized void method (){...}. (2)同步語句。 用synchronized 關鍵字修飾的程序塊稱同步語句或同步塊。 當需要調(diào)用某個類中的方法,而該類沒有同步方法,但又必須實現(xiàn)同步時,就要采用同步語句。 只要將對這個類定義的方法的調(diào)用放入一個synchronized 塊內(nèi)就可以了,其格式為: synchronized (object){...},其中,object 是對同步對象的引用。從經(jīng)過線程同步機制定義后的代碼形式可以看出:在對共享資源進行訪問的方法訪問屬性關鍵字(public)后附加同步定義關鍵字synchronized,使得同步方法在對共享資源訪問的時候,為這些敏感資源附加共享鎖來控制方法執(zhí)行期間的資源獨占性,實現(xiàn)了應用系統(tǒng)數(shù)據(jù)資源的一致性管理和維護。
(五)避免死鎖
死鎖是一個經(jīng)典的多線程問題,它是一種少見的、而且難于調(diào)試的錯誤,在兩個線程對兩個同步對象具有循環(huán)依賴時,就會出現(xiàn)死鎖。因為不同的線程都在等待那些根本不可能被釋放的鎖,從而導致所有的工作都無法完成。死鎖不是資源不夠引起的,而是由線程的調(diào)度引起的,對于死鎖可用下述方法解決: (1)嘗試在盡可能短的時間內(nèi)執(zhí)行鎖定的代碼,占用時間越長,另一個線程出現(xiàn)和需要對象的可能性越大;(2)當你從另一個被同步的方法中激活被同步的方法時要小心,最好是清楚地定義每個線程的任務,并考慮使用什么數(shù)據(jù)和什么時間使用。
二、多線程的應用
在實際應用中,線程使用的范圍很廣,可用于控制實時數(shù)據(jù)處理、快速的網(wǎng)絡服務,還有更快的圖像繪制和打印,以及數(shù)據(jù)庫中數(shù)據(jù)的取回和處理等等。 在Java 中有些線程在不停運行,提供一些基本服務,其中一個典型的例子就是垃圾收集線程( GarbageCollectionThread )。 該線程由Java虛擬機(JVM )提供,由一個中心線程控制執(zhí)行,它掃描程序中不再被訪問的變量,將其所占的系統(tǒng)資源釋放給系統(tǒng),當Java 線程請求存儲時,如果Java 虛擬機不能分配足夠的存儲塊來滿足這個請求,則可以說出現(xiàn)了分配失敗,這就不可避免要進行垃圾回收。結(jié)合Java 的多線程應用及其它特性,如平臺無關性、對網(wǎng)絡強有力的支持,特別是Sun 公司對Java 語言進行了擴充,引入了遠程方法調(diào)用RMI 機制,它提供了在并行計算環(huán)境下應用Java 語言的一些基本功能。由此可見,Java 在分布式計算、并行計算方面將大有作為。
三、使用多線程應注意的問題
利用多線程的并發(fā)執(zhí)行特點,無疑會加快程序的運行,提高CPU 、內(nèi)存等系統(tǒng)資源的使用效率,但在應用多線程時,要注意同時運行在內(nèi)存中的多個線程之間的關系。由于線程執(zhí)行不受建立先后的約束,線程間共享數(shù)據(jù)可能會出現(xiàn)程序員想象不到的事情,還有多個線程同時訪問修改同一個數(shù)據(jù)、資源競爭和死鎖問題,都需要程序員周密的考慮。總體來說,在程序中使用多線程需要考慮以下幾個問題:
(一)創(chuàng)建線程需要占用更多的內(nèi)存資源;
(二)創(chuàng)建線程需要增加CPU 跟蹤線程、切換線程的時間開銷;
(三)多線程編程必須考慮資源共享問題、同時訪問通訊端口問題以及系統(tǒng)資源競爭和死鎖問題;
(四)同一任務下的多個線程同時訪問和修改某個數(shù)據(jù)時,要考慮數(shù)據(jù)的安全問題。由此看出,線程不是越多越好,而是要適度,并且對多線程編程的程序員也提高了要求??傊?,多線程會使軟件編程變得更加靈活,同時也給程序員帶來了更大的機遇和挑戰(zhàn)。
四、結(jié)束語
由于Java 的多線程功能齊全, 它帶來的好處也是顯然易見的。在開發(fā)難易程度和性能上都比單線程要好。另一方面,由于多線程還沒有充分利用基本操作系統(tǒng)的這一功能,對于不同的系統(tǒng), 上面的程序可能會出現(xiàn)截然不同的結(jié)果, 這使編程者偶會感到迷惑不解。
實際運用時,要充分考慮多線程編程的復雜性以及線程切換開銷帶來的多線程程序的低效性,要注意是否需要多線程,就是要看這是否也是它的內(nèi)在特點,只有當完全符合多線程的特點時,多線程機制對線程間通信和線程管理的強大支持才能有用武之地,這時使用多線程才是值得的。實際上, 無論是系統(tǒng)級還是語言級的多線程, 如果操作系統(tǒng)本身不支持多線程,Java 的多線程就可能只是受限的或不完全的多線程。
參考文獻
[1]盧俊嶺,師軍,基于Java 的多線程機制[J]陜西師范大學學報(自然科學版) ,2000, (4)
[2]馬紅霞,張春芳,JAVA 與多線程程序設計[J]河北工業(yè)科技,2004, (4)
[3]王克宏,Java 技術教程(基礎篇) [M] 北京:清華大學出版社,2005
[4]耿祥義,張躍平,Java 2 實用教程[M]北京:清華大學出版社,2006.8