文章詳情頁(yè)
如果我是國(guó)王:關(guān)于解決 Java 編程語(yǔ)言線程問題的建議
瀏覽:86日期:2024-06-28 14:03:57
內(nèi)容: 如果我是國(guó)王:關(guān)于解決 Java 編程語(yǔ)言線程問題的建議Allen Holub自由撰稿人 內(nèi)容: task(任務(wù)) 的概念 synchronized 關(guān)鍵字 wait 和 notify 方法 修定 thread 類 線程間的協(xié)作 讀寫鎖 部分創(chuàng)建的對(duì)象 volatile 關(guān)鍵字 訪問的問題 后臺(tái)程序的突然結(jié)束 重新引入 stop、suspend 和 resume 被阻斷的 I/O threadGroup 類 總結(jié) 參考資料 作者簡(jiǎn)介 Allen Holub 指出,Java 編程語(yǔ)言的線程模型可能是此語(yǔ)言中最薄弱的部分。它完全不適合實(shí)際復(fù)雜程序的要求,而且也完全不是面向?qū)ο蟮?。本文建議對(duì) Java 語(yǔ)言進(jìn)行重大修改和補(bǔ)充,以解決這些問題。Java 語(yǔ)言的線程模型是此語(yǔ)言的一個(gè)最難另人滿意的部分。盡管 Java 語(yǔ)言本身就支持線程編程是件好事,但是它對(duì)線程的語(yǔ)法和類包的支持太少,只能適用于極小型的應(yīng)用環(huán)境。關(guān)于 Java 線程編程的大多數(shù)書籍都長(zhǎng)篇累牘地指出了 Java 線程模型的缺陷,并提供了解決這些問題的急救包(Band-Aid/邦迪創(chuàng)可貼)類庫(kù)。我稱這些類為急救包,是因?yàn)樗鼈兯芙鉀Q的問題本應(yīng)是由 Java 語(yǔ)言本身語(yǔ)法所包含的。從長(zhǎng)遠(yuǎn)來看,以語(yǔ)法而不是類庫(kù)方法,將能產(chǎn)生更高效的代碼。這是因?yàn)榫幾g器和 Java 虛擬器 (JVM) 能一同優(yōu)化程序代碼,而這些優(yōu)化對(duì)于類庫(kù)中的代碼是很難或無法實(shí)現(xiàn)的。在我的《Taming Java Threads》(請(qǐng)參閱參考資料)書中以及本文中,我進(jìn)一步建議對(duì) Java 編程語(yǔ)言本身進(jìn)行一些修改,以使得它能夠真正解決這些線程編程的問題。本文和我這本書的主要區(qū)別是,我在撰寫本文時(shí)進(jìn)行了更多的思考, 所以對(duì)書中的提議加以了提高。這些建議只是嘗試性的 -- 只是我個(gè)人對(duì)這些問題的想法,而且實(shí)現(xiàn)這些想法需要進(jìn)行大量的工作以及同行們的評(píng)價(jià)。但這是畢竟是一個(gè)開端,我有意為解決這些問題成立一個(gè)專門的工作組,如果您感興趣,請(qǐng)發(fā) e-mail 到 threading@holub.com。一旦我真正著手進(jìn)行,我就會(huì)給您發(fā)通知。這里提出的建議是非常大膽的。有些人建議對(duì) Java 語(yǔ)言規(guī)范 (JLS)(請(qǐng)參閱參考資料)進(jìn)行細(xì)微和少量的修改以解決當(dāng)前模糊的 JVM 行為,但是我卻想對(duì)其進(jìn)行更為徹底的改進(jìn)。在實(shí)際草稿中,我的許多建議包括為此語(yǔ)言引入新的關(guān)鍵字。雖然通常要求不要突破一個(gè)語(yǔ)言的現(xiàn)有代碼是正確的,但是如果該語(yǔ)言的并不是要保持不變以至于過時(shí)的話,它就必須能引入新的關(guān)鍵字。為了使引入的關(guān)鍵字與現(xiàn)有的標(biāo)識(shí)符不產(chǎn)生沖突,經(jīng)過細(xì)心考慮,我將使用一個(gè) ($) 字符,而這個(gè)字符在現(xiàn)有的標(biāo)識(shí)符中是非法的。(例如,使用 $task, 而不是 task)。此時(shí)需要編譯器的命令行開關(guān)提供支持,能使用這些關(guān)鍵字的變體,而不是忽略這個(gè)美元符號(hào)。task(任務(wù))的概念 Java 線程模型的根本問題是它完全不是面向?qū)ο蟮?。面向?qū)ο?(OO) 設(shè)計(jì)人員根本不按線程角度考慮問題;他們考慮的是同步信息異步信息(同步信息被立即處理 -- 直到信息處理完成才返回消息句柄;異步信息收到后將在后臺(tái)處理一段時(shí)間-- 而早在信息處理結(jié)束前就返回消息句柄)。Java 編程語(yǔ)言中的 Toolkit.getImage() 方法就是異步信息的一個(gè)好例子。getImage() 的消息句柄將被立即返回,而不必等到整個(gè)圖像被后臺(tái)線程取回。這是面向?qū)ο?(OO) 的處理方法。但是,如前所述,Java 的線程模型是非面向?qū)ο蟮?。一個(gè) Java 編程語(yǔ)言線程實(shí)際上只是一個(gè) run() 過程,它調(diào)用了其它的過程。在這里就根本沒有對(duì)象、異步或同步信息以及其它概念。對(duì)于此問題,在我的書中深入討論過的一個(gè)解決方法是,使用一個(gè) Active_object。 active 對(duì)象是可以接收異步請(qǐng)求的對(duì)象,它在接收到請(qǐng)求后的一段時(shí)間內(nèi)以后臺(tái)方式得以處理。在 Java 編程語(yǔ)言中,一個(gè)請(qǐng)求可被封裝在一個(gè)對(duì)象中。例如,你可以把一個(gè)通過 Runnable接口實(shí)現(xiàn)的實(shí)例傳送給此 active 對(duì)象,該接口的 run() 方法封裝了需要完成的工作。該 runnable 對(duì)象被此 active 對(duì)象排入到隊(duì)列中,當(dāng)輪到它執(zhí)行時(shí),active 對(duì)象使用一個(gè)后臺(tái)線程來執(zhí)行它。在一個(gè) active 對(duì)象上運(yùn)行的異步信息實(shí)際上是同步的,因?yàn)樗鼈儽灰粋€(gè)單一的服務(wù)線程按順序從隊(duì)列中取出并執(zhí)行。因此,使用一個(gè) active 對(duì)象以一種更為過程化的模型可以消除大多數(shù)的同步問題。在某種意義上,Java 編程語(yǔ)言的整個(gè) Swing/AWT 子系統(tǒng)是一個(gè) active 對(duì)象。向一個(gè) Swing 隊(duì)列傳送一條訊息的唯一安全的途徑是,調(diào)用一個(gè)類似 SwingUtilities.invokeLater() 的方法,這樣就在 Swing 事件隊(duì)列上發(fā)送了一個(gè) runnable 對(duì)象,當(dāng)輪到它執(zhí)行時(shí), Swing 事件處理線程將會(huì)處理它。那么我的第一個(gè)建議是,向 Java 編程語(yǔ)言中加入一個(gè) task(任務(wù))的概念,從而將active 對(duì)象集成到語(yǔ)言中。( task的概念是從 Intel 的 RMX 操作系統(tǒng)和 Ada 編程語(yǔ)言借鑒過來的。大多數(shù)實(shí)時(shí)操作系統(tǒng)都支持類似的概念。)一個(gè)任務(wù)有一個(gè)內(nèi)置的 active 對(duì)象分發(fā)程序,并自動(dòng)管理那些處理異步信息的全部機(jī)制。定義一個(gè)任務(wù)和定義一個(gè)類基本相同,不同的只是需要在任務(wù)的方法前加一個(gè) asynchronous 修飾符來指示 active 對(duì)象的分配程序在后臺(tái)處理這些方法。請(qǐng)參考我的書中第九章的基于類方法,再看以下的 file_io 類,它使用了在《Taming Java Threads》中所討論的 Active_object 類來實(shí)現(xiàn)異步寫操作: interface Exception_handler { void handle_exception( Throwable e ); } class File_io_task { Active_object dispatcher = new Active_object(); final OutputStream file; final Exception_handler handler; File_io_task( String file_name, Exception_handler handler ) throws IOException { file = new FileOutputStream( file_name ); this.handler = handler; } public void write( final byte[] bytes ) { // The following call asks the active-object dispatcher // to enqueue the Runnable object on its request // queue. A thread associated with the active object // dequeues the runnable objects and executes them // one at a time. dispatcher.dispatch ( new Runnable() { public void run() { try { byte[] copy new byte[ bytes.length ]; System.arrayCopy( bytes, 0, copy, 0, bytes.length ); file.write( copy ); } catch( Throwable problem ) { handler.handle_exception( problem ); } } } ); } } 所有的寫請(qǐng)求都用一個(gè) dispatch() 過程調(diào)用被放在 active-object 的輸入隊(duì)列中排隊(duì)。在后臺(tái)處理這些異步信息時(shí)出現(xiàn)的任何異常 (exception) 都由 Exception_handler 對(duì)象處理,此 Exception_handler 對(duì)象被傳送到 File_io_task 的構(gòu)造函數(shù)中。您要寫內(nèi)容到文件時(shí),代碼如下: File_io_task io = new File_io_task ( 'foo.txt' new Exception_handler { public void handle( Throwable e ){ e.printStackTrace();} } ); //... io.write( some_bytes ); 這種基于類的處理方法,其主要問題是太復(fù)雜了 -- 對(duì)于一個(gè)這樣簡(jiǎn)單的操作,代碼太雜了。向 Java 語(yǔ)言引入 $task 和 $asynchronous 關(guān)鍵字后,就可以按下面這樣重寫以前的代碼: $task File_io $error{ $.printStackTrace(); } { OutputStream file; File_io( String file_name ) throws IOException { file = new FileOutputStream( file_name ); } asynchronous public write( byte[] bytes ) { file.write( bytes ); } } 注意,異步方法并沒有指定返回值,因?yàn)槠渚浔鷮⒈涣⒓捶祷?,而不用等到?qǐng)求的操作處理完成后。所以,此時(shí)沒有合理的返回值。對(duì)于派生出的模型, $task 關(guān)鍵字和 class 一樣同效:$task 可以實(shí)現(xiàn)接口、繼承類和繼承的其它任務(wù)。標(biāo)有 asynchronous 關(guān)鍵字的方法由$task 在后臺(tái)處理。其它的方法將同步運(yùn)行,就像在類中一樣。$task 關(guān)鍵字可以用一個(gè)可選的 $error 從句修飾 (如上所示), 它表明對(duì)任何無法被異步方法本身捕捉的異常將有一個(gè)缺省的處理程序。我使用 $ 來代表被拋出的異常對(duì)象。如果沒有指定 $error 從句,就將打印出一個(gè)合理的出錯(cuò)信息(很可能是堆棧跟蹤信息)。注意,為確保線程安全,異步方法的參數(shù)必須是不變 (immutable) 的。運(yùn)行時(shí)系統(tǒng)應(yīng)通過相關(guān)語(yǔ)義來保證這種不變性(簡(jiǎn)單的復(fù)制通常是不夠的)。所有的 task 對(duì)象必須支持一些偽信息 (pseudo-message),例如: some_task.close() 在此調(diào)用后發(fā)送的任何異步信息都產(chǎn)生一個(gè) TaskClosedException。但是,在 active 對(duì)象隊(duì)列上等候的消息仍能被提供。 some_task.join() 調(diào)用程序被阻斷,直到此任務(wù)關(guān)閉、而且所有未完成的請(qǐng)求都被處理完畢。 除了常用的修飾符(public 等),task 關(guān)鍵字還應(yīng)接受一個(gè) $pooled(n) 修飾符,它導(dǎo)致 task 使用一個(gè)線程池,而不是使用單個(gè)線程來運(yùn)行異步請(qǐng)求。n 指定了所需線程池的大??;必要時(shí),此線程池可以增加,但是當(dāng)不再需要線程時(shí),它應(yīng)該縮到原來的大小。偽域 (pseudo-field) $pool_size 返回在 $pooled(n) 中指定的原始 n 參數(shù)值。在《Taming Java Threads》的第八章中,我給出了一個(gè)服務(wù)器端的 socket 處理程序,作為線程池的例子。它是關(guān)于使用線程池的任務(wù)的一個(gè)好例子。其基本思路是產(chǎn)生一個(gè)獨(dú)立對(duì)象,它的任務(wù)是監(jiān)控一個(gè)服務(wù)器端的 socket。每當(dāng)一個(gè)客戶機(jī)連接到服務(wù)器時(shí),服務(wù)器端的對(duì)象會(huì)從池中抓取一個(gè)預(yù)先創(chuàng)建的睡眠線程,并把此線程設(shè)置為服務(wù)于客戶端連接。socket 服務(wù)器會(huì)產(chǎn)出一個(gè)額外的客戶服務(wù)線程,但是當(dāng)連接關(guān)閉時(shí),這些額外的線程將被刪除。實(shí)現(xiàn) socket 服務(wù)器的推薦語(yǔ)法如下: public $pooled(10) $task Client_handler { PrintWriter log = new PrintWriter( System.out ); public asynchronous void handle( Socket connection_to_the_client ) { log.println('writing'); // client-handling code goes here. Every call to // handle() is executed on its own thread, but 10 // threads are pre-created for this purpose. Additional // threads are created on an as-needed basis, but are // discarded when handle() returns. } } $task Socket_server { ServerSocket server; Client_handler client_handlers = new Client_handler(); public Socket_server( int port_number ) { server = new ServerSocket(port_number); } public $asynchronous listen(Client_handler client) { // This method is executed on its own thread. while( true ) { client_handlers.handle( server.accept() ); } } } //... Socket_server = new Socket_server( the_port_number ); server.listen() Socket_server 對(duì)象使用一個(gè)獨(dú)立的后臺(tái)線程處理異步的 listen() 請(qǐng)求,它封裝 socket 的“接受循環(huán)。當(dāng)每個(gè)客戶端連接時(shí),listen() 請(qǐng)求一個(gè) Client_handler 通過調(diào)用 handle() 來處理請(qǐng)求。每個(gè) handle() 請(qǐng)求在它們自己的線程中執(zhí)行(因?yàn)檫@是一個(gè) $pooled 任務(wù))。注意,每個(gè)傳送到 $pooled $task 的異步消息實(shí)際上都使用它們自己的線程來處理。典型情況下,由于一個(gè) $pooled $task 用于實(shí)現(xiàn)一個(gè)自主操作;所以對(duì)于解決與訪問狀態(tài)變量有關(guān)的潛在的同步問題,最好的解決方法是在 $asynchronous 方法中使用 this 是指向的對(duì)象的一個(gè)獨(dú)有副本。這就是說,當(dāng)向一個(gè) $pooled $task 發(fā)送一個(gè)異步請(qǐng)求時(shí),將執(zhí)行一個(gè) clone() 操作,并且此方法的 this 指針會(huì)指向此克隆對(duì)象。線程之間的通信可通過對(duì) static 區(qū)的同步訪問實(shí)現(xiàn)。改進(jìn) synchronized 雖然在多數(shù)情況下, $task 消除了同步操作的要求,但是不是所有的多線程系統(tǒng)都用任務(wù)來實(shí)現(xiàn)。所以,還需要改進(jìn)現(xiàn)有的線程模塊。 synchronized 關(guān)鍵字有下列缺點(diǎn):無法指定一個(gè)超時(shí)值。 無法中斷一個(gè)正在等待請(qǐng)求鎖的線程。 無法安全地請(qǐng)求多個(gè)鎖 。(多個(gè)鎖只能以依次序獲得。) 解決這些問題的辦法是:擴(kuò)展 synchronized 的語(yǔ)法,使它支持多個(gè)參數(shù)和能接受一個(gè)超時(shí)說明(在下面的括弧中指定)。下面是我希望的語(yǔ)法: synchronized(x && y && z) 獲得 x、y 和 z 對(duì)象的鎖。 synchronized(x || y || z) 獲得 x、y 或 z 對(duì)象的鎖。 synchronized( (x && y ) || z) 對(duì)于前面代碼的一些擴(kuò)展。 synchronized(...)[1000] 設(shè)置 1 秒超時(shí)以獲得一個(gè)鎖。 synchronized[1000] f(){...} 在進(jìn)入 f() 函數(shù)時(shí)獲得 this 的鎖,但可有 1 秒超時(shí)。 TimeoutException 是 RuntimeException 派生類,它在等待超時(shí)后即被拋出。超時(shí)是需要的,但還不足以使代碼強(qiáng)壯。您還需要具備從外部中止請(qǐng)求鎖等待的能力。所以,當(dāng)向一個(gè)等待鎖的線程傳送一個(gè) interrupt() 方法后,此方法應(yīng)拋出一個(gè) SynchronizationException 對(duì)象,并中斷等待的線程。這個(gè)異常應(yīng)是 RuntimeException 的一個(gè)派生類,這樣不必特別處理它。對(duì) synchronized 語(yǔ)法這些推薦的更改方法的主要問題是,它們需要在二進(jìn)制代碼級(jí)上修改。而目前這些代碼使用進(jìn)入監(jiān)控(enter-monitor)和退出監(jiān)控(exit-monitor)指令來實(shí)現(xiàn) synchronized。而這些指令沒有參數(shù),所以需要擴(kuò)展二進(jìn)制代碼的定義以支持多個(gè)鎖定請(qǐng)求。但是這種修改不會(huì)比在 Java 2 中修改 Java 虛擬機(jī)的更輕松,但它是向下兼容現(xiàn)存的 Java 代碼。另一個(gè)可解決的問題是最常見的死鎖情況,在這種情況下,兩個(gè)線程都在等待對(duì)方完成某個(gè)操作。設(shè)想下面的一個(gè)例子(假設(shè)的):class Broken{ Object lock1 = new Object(); Object lock2 = new Object(); void a() { synchronized( lock1 ) { synchronized( lock2 ) { // do something } } } void b() { synchronized( lock2 ) { synchronized( lock1 ) { // do something } } } 設(shè)想一個(gè)線程調(diào)用 a(),但在獲得 lock1之后在獲得 lock2 之前被剝奪運(yùn)行權(quán)。第二個(gè)線程進(jìn)入運(yùn)行,調(diào)用 b(),獲得了 lock2,但是由于第一個(gè)線程占用 lock1,所以它無法獲得 lock1,所以它隨后處于等待狀態(tài)。此時(shí)第一個(gè)線程被喚醒,它試圖獲得 lock2,但是由于被第二個(gè)線程占據(jù),所以無法獲得。此時(shí)出現(xiàn)死鎖。下面的 synchronize-on-multiple-objects 的語(yǔ)法可解決這個(gè)問題: //... void a() { synchronized( lock1 && lock2 ) { } } void b() { synchronized( lock2 && lock3 ) { } } 編譯器(或虛擬機(jī))會(huì)重新排列請(qǐng)求鎖的順序,使 lock1 總是被首先獲得,這就消除了死鎖。但是,這種方法對(duì)多線程不一定總成功,所以得提供一些方法來自動(dòng)打破死鎖。一個(gè)簡(jiǎn)單的辦法就是在等待第二個(gè)鎖時(shí)常釋放已獲得的鎖。這就是說,應(yīng)采取如下的等待方式,而不是永遠(yuǎn)等待: while( true ) { try { synchronized( some_lock )[10] { // do the work here. break; } } catch( TimeoutException e ) { continue; } } 如果等待鎖的每個(gè)程序使用不同的超時(shí)值,就可打破死鎖而其中一個(gè)線程就可運(yùn)行。我建議用以下的語(yǔ)法來取代前面的代碼: synchronized( some_lock )[] { // do the work here. } synchronized 語(yǔ)句將永遠(yuǎn)等待,但是它時(shí)常會(huì)放棄已獲得的鎖以打破潛在的死鎖可能。在理想情況下,每個(gè)重復(fù)等待的超時(shí)值比前一個(gè)相差一隨機(jī)值。改進(jìn) wait() 和 notify() wait()/notify() 系統(tǒng)也有一些問題:無法檢測(cè) wait() 是正常返回還是因超時(shí)返回。 無法使用傳統(tǒng)條件變量來實(shí)現(xiàn)處于一個(gè)“信號(hào)(signaled)狀態(tài)。 太容易發(fā)生嵌套的監(jiān)控(monitor)鎖定。 超時(shí)檢測(cè)問題可以通過重新定義 wait() 使它返回一個(gè) boolean 變量 (而不是 void ) 來解決。一個(gè) true 返回值指示一個(gè)正常返回,而 false 指示因超時(shí)返回?;跔顟B(tài)的條件變量的概念是很重要的。如果此變量被設(shè)置成 false 狀態(tài),那么等待的線程將要被阻斷,直到此變量進(jìn)入 true 狀態(tài);任何等待 true 的條件變量的等待線程會(huì)被自動(dòng)釋放。(在這種情況下,wait() 調(diào)用不會(huì)發(fā)生阻斷。)。通過如下擴(kuò)展 notify() 的語(yǔ)法,可以支持這個(gè)功能: notify(); 釋放所有等待的線程,而不改變其下面的條件變量的狀態(tài)。 notify(true); 把條件變量的狀態(tài)設(shè)置為 true 并釋放任何等待的進(jìn)程。其后對(duì)于 wait() 的調(diào)用不會(huì)發(fā)生阻斷。 notify(false); 把條件變量的狀態(tài)設(shè)置為 false (其后對(duì)于 wait() 的調(diào)用會(huì)發(fā)生阻斷)。 嵌套監(jiān)控鎖定問題非常麻煩,我并沒有簡(jiǎn)單的解決辦法。嵌套監(jiān)控鎖定是一種死鎖形式,當(dāng)某個(gè)鎖的占有線程在掛起其自身之前不釋放鎖時(shí),會(huì)發(fā)生這種嵌套監(jiān)控封鎖。下面是此問題的一個(gè)例子(還是假設(shè)的),但是實(shí)際的例子是非常多的:class Stack{ LinkedList list = new LinkedList(); public synchronized void push(Object x) { synchronized(list) { list.addLast( x ); notify(); } } public synchronized Object pop() { synchronized(list) { if( list.size()
標(biāo)簽:
Java
相關(guān)文章:
1. Java commons-httpclient如果實(shí)現(xiàn)get及post請(qǐng)求2. 如果你恨一個(gè)程序員,忽悠他去做iOS開發(fā)3. Java如果通過jdbc操作連接oracle數(shù)據(jù)庫(kù)4. Java如果在try里面執(zhí)行return還會(huì)不會(huì)執(zhí)行finally5. 如果沒有 Android,世界會(huì)怎樣?6. 如果沒有 Android 世界會(huì)是什么樣子?7. 解決SpringBoot返回結(jié)果如果為null或空值不顯示處理問題8. 解決django的template中如果無法引用MEDIA_URL問題
排行榜
