編寫多線程的 Java 應(yīng)用程序如何避免當(dāng)前編程中最常見的問題 ![]() |
![]() |
![]() |
級(jí)別: 初級(jí) Alex Roetter (aroetter@CS.Stanford.edu), Teton Data Systems 的軟件工程師 2001 年 2 月 01 日 Java Thread API 允許程序員編寫具有多處理機(jī)制優(yōu)點(diǎn)的應(yīng)用程序,在后臺(tái)處理任務(wù)的同時(shí)保持用戶所需的交互感。Alex Roetter 介紹了 Java Thread API,并概述多線程可能引起的問題以及常見問題的解決方案。 幾乎所有使用 AWT 或 Swing 編寫的畫圖程序都需要多線程。但多線程程序會(huì)造成許多困難,剛開始編程的開發(fā)者常常會(huì)發(fā)現(xiàn)他們被一些問題所折磨,例如不正確的程序行為或死鎖。 在本文中,我們將探討使用多線程時(shí)遇到的問題,并提出那些常見陷阱的解決方案。 一個(gè)程序或進(jìn)程能夠包含多個(gè)線程,這些線程可以根據(jù)程序的代碼執(zhí)行相應(yīng)的指令。多線程看上去似乎在并行執(zhí)行它們各自的工作,就像在一臺(tái)計(jì)算機(jī)上運(yùn)行著多個(gè)處理機(jī)一樣。在多處理機(jī)計(jì)算機(jī)上實(shí)現(xiàn)多線程時(shí),它們確實(shí) 可以并行工作。和進(jìn)程不同的是,線程共享地址空間。也就是說,多個(gè)線程能夠讀寫相同的變量或數(shù)據(jù)結(jié)構(gòu)。 編寫多線程程序時(shí),你必須注意每個(gè)線程是否干擾了其他線程的工作。可以將程序看作一個(gè)辦公室,如果不需要共享辦公室資源或與其他人交流,所有職員就會(huì)獨(dú)立并行地工作。某個(gè)職員若要和其他人交談,當(dāng)且僅當(dāng)該職員在“聽”且他們兩說同樣的語言。此外,只有在復(fù)印機(jī)空閑且處于可用狀態(tài)(沒有僅完成一半的復(fù)印工作,沒有紙張阻塞等問題)時(shí),職員才能夠使用它。在這篇文章中你將看到,在 Java 程序中互相協(xié)作的線程就好像是在一個(gè)組織良好的機(jī)構(gòu)中工作的職員。 在多線程程序中,線程可以從準(zhǔn)備就緒隊(duì)列中得到,并在可獲得的系統(tǒng) CPU 上運(yùn)行。操作系統(tǒng)可以將線程從處理器移到準(zhǔn)備就緒隊(duì)列或阻塞隊(duì)列中,這種情況可以認(rèn)為是處理器“掛起”了該線程。同樣,Java 虛擬機(jī) (JVM) 也可以控制線程的移動(dòng)――在協(xié)作或搶先模型中――從準(zhǔn)備就緒隊(duì)列中將進(jìn)程移到處理器中,于是該線程就可以開始執(zhí)行它的程序代碼。 協(xié)作式線程 模型允許線程自己決定什么時(shí)候放棄處理器來等待其他的線程。程序開發(fā)員可以精確地決定某個(gè)線程何時(shí)會(huì)被其他線程掛起,允許它們與對(duì)方有效地合作。缺點(diǎn)在于某些惡意或是寫得不好的線程會(huì)消耗所有可獲得的 CPU 時(shí)間,導(dǎo)致其他線程“饑餓”。 在 搶占式線程 模型中,操作系統(tǒng)可以在任何時(shí)候打斷線程。通常會(huì)在它運(yùn)行了一段時(shí)間(就是所謂的一個(gè)時(shí)間片)后才打斷它。這樣的結(jié)果自然是沒有線程能夠不公平地長時(shí)間霸占處理器。然而,隨時(shí)可能打斷線程就會(huì)給程序開發(fā)員帶來其他麻煩。同樣使用辦公室的例子,假設(shè)某個(gè)職員搶在另一人前使用復(fù)印機(jī),但打印工作在未完成的時(shí)候離開了,另一人接著使用復(fù)印機(jī)時(shí),該復(fù)印機(jī)上可能就還有先前那名職員留下來的資料。搶占式線程模型要求線程正確共享資源,協(xié)作式模型卻要求線程共享執(zhí)行時(shí)間。由于 JVM 規(guī)范并沒有特別規(guī)定線程模型,Java 開發(fā)員必須編寫可在兩種模型上正確運(yùn)行的程序。在了解線程以及線程間通訊的一些方面之后,我們可以看到如何為這兩種模型設(shè)計(jì)程序。
為了使用 Java 語言創(chuàng)建線程,你可以生成一個(gè)
大多數(shù)應(yīng)用程序要求線程互相通信來同步它們的動(dòng)作。在 Java 程序中最簡單實(shí)現(xiàn)同步的方法就是上鎖。為了防止同時(shí)訪問共享資源,線程在使用資源的前后可以給該資源上鎖和開鎖。假想給復(fù)印機(jī)上鎖,任一時(shí)刻只有一個(gè)職員擁有鑰匙。若沒有鑰匙就不能使用復(fù)印機(jī)。給共享變量上鎖就使得 Java 線程能夠快速方便地通信和同步。某個(gè)線程若給一個(gè)對(duì)象上了鎖,就可以知道沒有其他線程能夠訪問該對(duì)象。即使在搶占式模型中,其他線程也不能夠訪問此對(duì)象,直到上鎖的線程被喚醒、完成工作并開鎖。那些試圖訪問一個(gè)上鎖對(duì)象的線程通常會(huì)進(jìn)入睡眠狀態(tài),直到上鎖的線程開鎖。一旦鎖被打開,這些睡眠進(jìn)程就會(huì)被喚醒并移到準(zhǔn)備就緒隊(duì)列中。 在 Java 編程中,所有的對(duì)象都有鎖。線程可以使用
Fine-grain 鎖
若為了在方法級(jí)上同步,不能將整個(gè)方法聲明為
通常情況下,可能有多個(gè)線程需要訪問數(shù)目很少的資源。假想在服務(wù)器上運(yùn)行著若干個(gè)回答客戶端請(qǐng)求的線程。這些線程需要連接到同一數(shù)據(jù)庫,但任一時(shí)刻只能獲得一定數(shù)目的數(shù)據(jù)庫連接。你要怎樣才能夠有效地將這些固定數(shù)目的數(shù)據(jù)庫連接分配給大量的線程?一種控制訪問一組資源的方法(除了簡單地上鎖之外),就是使用眾所周知的信號(hào)量計(jì)數(shù) (counting semaphore)。 信號(hào)量計(jì)數(shù)將一組可獲得資源的管理封裝起來。信號(hào)量是在簡單上鎖的基礎(chǔ)上實(shí)現(xiàn)的,相當(dāng)于能令線程安全執(zhí)行,并初始化為可用資源個(gè)數(shù)的計(jì)數(shù)器。例如我們可以將一個(gè)信號(hào)量初始化為可獲得的數(shù)據(jù)庫連接個(gè)數(shù)。一旦某個(gè)線程獲得了信號(hào)量,可獲得的數(shù)據(jù)庫連接數(shù)減一。線程消耗完資源并釋放該資源時(shí),計(jì)數(shù)器就會(huì)加一。當(dāng)信號(hào)量控制的所有資源都已被占用時(shí),若有線程試圖訪問此信號(hào)量,則會(huì)進(jìn)入阻塞狀態(tài),直到有可用資源被釋放。 信號(hào)量最常見的用法是解決“消費(fèi)者-生產(chǎn)者問題”。當(dāng)一個(gè)線程進(jìn)行工作時(shí),若另外一個(gè)線程訪問同一共享變量,就可能產(chǎn)生此問題。消費(fèi)者線程只能在生產(chǎn)者線程完成生產(chǎn)后才能夠訪問數(shù)據(jù)。使用信號(hào)量來解決這個(gè)問題,就需要?jiǎng)?chuàng)建一個(gè)初始化為零的信號(hào)量,從而讓消費(fèi)者線程訪問此信號(hào)量時(shí)發(fā)生阻塞。每當(dāng)完成單位工作時(shí),生產(chǎn)者線程就會(huì)向該信號(hào)量發(fā)信號(hào)(釋放資源)。每當(dāng)消費(fèi)者線程消費(fèi)了單位生產(chǎn)結(jié)果并需要新的數(shù)據(jù)單元時(shí),它就會(huì)試圖再次獲取信號(hào)量。因此信號(hào)量的值就總是等于生產(chǎn)完畢可供消費(fèi)的數(shù)據(jù)單元數(shù)。這種方法比采用消費(fèi)者線程不停檢查是否有可用數(shù)據(jù)單元的方法要高效得多。因?yàn)橄M(fèi)者線程醒來后,倘若沒有找到可用的數(shù)據(jù)單元,就會(huì)再度進(jìn)入睡眠狀態(tài),這樣的操作系統(tǒng)開銷是非常昂貴的。 盡管信號(hào)量并未直接被 Java 語言所支持,卻很容易在給對(duì)象上鎖的基礎(chǔ)上實(shí)現(xiàn)。一個(gè)簡單的實(shí)現(xiàn)方法如下所示:
不幸的是,使用上鎖會(huì)帶來其他問題。讓我們來看一些常見問題以及相應(yīng)的解決方法:
判斷是搶占式還是協(xié)作式的線程模型,取決于虛擬機(jī)的實(shí)現(xiàn)者,并根據(jù)各種實(shí)現(xiàn)而不同。因此,Java 開發(fā)員必須編寫那些能夠在兩種模型上工作的程序。 正如前面所提到的,在搶占式模型中線程可以在代碼的任何一個(gè)部分的中間被打斷,除非那是一個(gè)原子操作代碼塊。原子操作代碼塊中的代碼段一旦開始執(zhí)行,就要在該線程被換出處理器之前執(zhí)行完畢。在 Java 編程中,分配一個(gè)小于 32 位的變量空間是一種原子操作,而此外象 而在協(xié)作式模型中,是否能保證線程正常放棄處理器,不掠奪其他線程的執(zhí)行時(shí)間,則完全取決于程序員。調(diào)用 正如你所想的那樣,將這些方法隨意放在代碼的某個(gè)地方,并不能夠保證正常工作。如果線程正擁有一個(gè)鎖(因?yàn)樗谝粋€(gè)同步方法或代碼塊中),則當(dāng)它調(diào)用 另外一個(gè)解決方法則是調(diào)用
在那些使用 Swing 和/或 AWT 包創(chuàng)建 GUI (用戶圖形界面)的 Java 程序中,AWT 事件句柄在它自己的線程中運(yùn)行。開發(fā)員必須注意避免將這些 GUI 線程與較耗時(shí)間的計(jì)算工作綁在一起,因?yàn)檫@些線程必須負(fù)責(zé)處理用戶時(shí)間并重繪用戶圖形界面。換句話來說,一旦 GUI 線程處于繁忙,整個(gè)程序看起來就象無響應(yīng)狀態(tài)。Swing 線程通過調(diào)用合適方法,通知那些 Swing callback (例如 Mouse Listener 和 Action Listener )。 這種方法意味著 listener 無論要做多少事情,都應(yīng)當(dāng)利用 listener callback 方法產(chǎn)生其他線程來完成此項(xiàng)工作。目的便在于讓 listener callback 更快速返回,從而允許 Swing 線程響應(yīng)其他事件。 如果一個(gè) Swing 線程不能夠同步運(yùn)行、響應(yīng)事件并重繪輸出,那怎么能夠讓其他的線程安全地修改 Swing 的狀態(tài)?正如上面提到的,Swing callback 在 Swing 線程中運(yùn)行。因此他們能修改 Swing 數(shù)據(jù)并繪到屏幕上。 但是如果不是 Swing callback 產(chǎn)生的變化該怎么辦呢?使用一個(gè)非 Swing 線程來修改 Swing 數(shù)據(jù)是不安全的。Swing 提供了兩個(gè)方法來解決這個(gè)問題:
Java 語言的設(shè)計(jì),使得多線程對(duì)幾乎所有的 Applet 都是必要的。特別是,IO 和 GUI 編程都需要多線程來為用戶提供完美的體驗(yàn)。如果依照本文所提到的若干基本規(guī)則,并在開始編程前仔細(xì)設(shè)計(jì)系統(tǒng)――包括它對(duì)共享資源的訪問等,你就可以避免許多常見和難以發(fā)覺的線程陷阱。
|
如果我是國王:關(guān)于解決 Java編程語言線程問題的建議![]() |
![]() |
![]() |
級(jí)別: 初級(jí) Allen Holub自由撰稿人 2000 年 10 月 01 日 Allen Holub 指出,Java 編程語言的線程模型可能是此語言中最薄弱的部分。它完全不適合實(shí)際復(fù)雜程序的要求,而且也完全不是面向?qū)ο蟮摹1疚慕ㄗh對(duì) Java 語言進(jìn)行重大修改和補(bǔ)充,以解決這些問題。 Java 語言的線程模型是此語言的一個(gè)最難另人滿意的部分。盡管 Java 語言本身就支持線程編程是件好事,但是它對(duì)線程的語法和類包的支持太少,只能適用于極小型的應(yīng)用環(huán)境。 關(guān)于 Java 線程編程的大多數(shù)書籍都長篇累牘地指出了 Java 線程模型的缺陷,并提供了解決這些問題的急救包(Band-Aid/邦迪創(chuàng)可貼)類庫。我稱這些類為急救包,是因?yàn)樗鼈兯芙鉀Q的問題本應(yīng)是由 Java 語言本身語法所包含的。從長遠(yuǎn)來看,以語法而不是類庫方法,將能產(chǎn)生更高效的代碼。這是因?yàn)榫幾g器和 Java 虛擬器 (JVM) 能一同優(yōu)化程序代碼,而這些優(yōu)化對(duì)于類庫中的代碼是很難或無法實(shí)現(xiàn)的。 在我的《 Taming Java Threads》(請(qǐng)參閱 參考資料 )書中以及本文中,我進(jìn)一步建議對(duì) Java 編程語言本身進(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 語言規(guī)范 (JLS)(請(qǐng)參閱 參考資料 )進(jìn)行細(xì)微和少量的修改以解決當(dāng)前模糊的 JVM 行為,但是我卻想對(duì)其進(jìn)行更為徹底的改進(jìn)。 在實(shí)際草稿中,我的許多建議包括為此語言引入新的關(guān)鍵字。雖然通常要求不要突破一個(gè)語言的現(xiàn)有代碼是正確的,但是如果該語言的并不是要保持不變以至于過時(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)。 Java 線程模型的根本問題是它完全不是面向?qū)ο蟮摹C嫦驅(qū)ο?(OO) 設(shè)計(jì)人員根本不按線程角度考慮問題;他們考慮的是 同步信息 異步 信息(同步信息被立即處理 -- 直到信息處理完成才返回消息句柄;異步信息收到后將在后臺(tái)處理一段時(shí)間 -- 而早在信息處理結(jié)束前就返回消息句柄)。Java 編程語言中的 這是面向?qū)ο?(OO) 的處理方法。但是,如前所述,Java 的線程模型是非面向?qū)ο蟮摹R粋€(gè) Java 編程語言線程實(shí)際上只是一個(gè) 對(duì)于此問題,在我的書中深入討論過的一個(gè)解決方法是,使用一個(gè) 在一個(gè) active 對(duì)象上運(yùn)行的異步信息實(shí)際上是同步的,因?yàn)樗鼈儽灰粋€(gè)單一的服務(wù)線程按順序從隊(duì)列中取出并執(zhí)行。因此,使用一個(gè) active 對(duì)象以一種更為過程化的模型可以消除大多數(shù)的同步問題。 在某種意義上,Java 編程語言的整個(gè) Swing/AWT 子系統(tǒng)是一個(gè) active 對(duì)象。向一個(gè) Swing 隊(duì)列傳送一條訊息的唯一安全的途徑是,調(diào)用一個(gè)類似 那么我的第一個(gè)建議是,向 Java 編程語言中加入一個(gè) task (任務(wù))的概念,從而將active 對(duì)象集成到語言中。( task的概念是從 Intel 的 RMX 操作系統(tǒng)和 Ada 編程語言借鑒過來的。大多數(shù)實(shí)時(shí)操作系統(tǒng)都支持類似的概念。) 一個(gè)任務(wù)有一個(gè)內(nèi)置的 active 對(duì)象分發(fā)程序,并自動(dòng)管理那些處理異步信息的全部機(jī)制。 定義一個(gè)任務(wù)和定義一個(gè)類基本相同,不同的只是需要在任務(wù)的方法前加一個(gè)
所有的寫請(qǐng)求都用一個(gè)
這種基于類的處理方法,其主要問題是太復(fù)雜了 -- 對(duì)于一個(gè)這樣簡單的操作,代碼太雜了。向 Java 語言引入
注意,異步方法并沒有指定返回值,因?yàn)槠渚浔鷮⒈涣⒓捶祷兀挥玫鹊秸?qǐng)求的操作處理完成后。所以,此時(shí)沒有合理的返回值。對(duì)于派生出的模型,
注意,為確保線程安全,異步方法的參數(shù)必須是不變 (immutable) 的。運(yùn)行時(shí)系統(tǒng)應(yīng)通過相關(guān)語義來保證這種不變性(簡單的復(fù)制通常是不夠的)。 所有的 task 對(duì)象必須支持一些偽信息 (pseudo-message),例如:
除了常用的修飾符( 在《 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ù)器的推薦語法如下:
注意,每個(gè)傳送到
雖然在多數(shù)情況下,
解決這些問題的辦法是:擴(kuò)展
超時(shí)是需要的,但還不足以使代碼強(qiáng)壯。您還需要具備從外部中止請(qǐng)求鎖等待的能力。所以,當(dāng)向一個(gè)等待鎖的線程傳送一個(gè) 對(duì) 另一個(gè)可解決的問題是最常見的死鎖情況,在這種情況下,兩個(gè)線程都在等待對(duì)方完成某個(gè)操作。設(shè)想下面的一個(gè)例子(假設(shè)的):
設(shè)想一個(gè)線程調(diào)用
編譯器(或虛擬機(jī))會(huì)重新排列請(qǐng)求鎖的順序,使 但是,這種方法對(duì)多線程不一定總成功,所以得提供一些方法來自動(dòng)打破死鎖。一個(gè)簡單的辦法就是在等待第二個(gè)鎖時(shí)常釋放已獲得的鎖。這就是說,應(yīng)采取如下的等待方式,而不是永遠(yuǎn)等待:
如果等待鎖的每個(gè)程序使用不同的超時(shí)值,就可打破死鎖而其中一個(gè)線程就可運(yùn)行。我建議用以下的語法來取代前面的代碼:
超時(shí)檢測(cè)問題可以通過重新定義 基于狀態(tài)的條件變量的概念是很重要的。如果此變量被設(shè)置成
嵌套監(jiān)控鎖定問題非常麻煩,我并沒有簡單的解決辦法。嵌套監(jiān)控鎖定是一種死鎖形式,當(dāng)某個(gè)鎖的占有線程在掛起其自身之前不釋放鎖時(shí),會(huì)發(fā)生這種嵌套監(jiān)控封鎖。下面是此問題的一個(gè)例子(還是假設(shè)的),但是實(shí)際的例子是非常多的:
此例中,在 在這個(gè)例子中,有很多明顯的辦法來解決問題:例如,對(duì)任何的方法都使用同步。但是在真實(shí)世界中,解決方法通常不是這么簡單。 一個(gè)可行的方法是,在 我也希望能等到下述復(fù)雜條件被實(shí)現(xiàn)的一天。例如:
其中
同時(shí)支持搶占式和協(xié)作式線程的能力在某些服務(wù)器應(yīng)用程序中是基本要求,尤其是在想使系統(tǒng)達(dá)到最高性能的情況下。我認(rèn)為 Java 編程語言在簡化線程模型上走得太遠(yuǎn)了,并且 Java 編程語言應(yīng)支持 Posix/Solaris 的“綠色(green)線程”和“輕便(lightweight)進(jìn)程”概念(在“( Taming Java Threads ”第一章中討論)。 這就是說,有些 Java 虛擬機(jī)的實(shí)現(xiàn)(例如在 NT 上的 Java 虛擬機(jī))應(yīng)在其內(nèi)部仿真協(xié)作式進(jìn)程,其它 Java 虛擬機(jī)應(yīng)仿真搶占式線程。而且向 Java 虛擬機(jī)加入這些擴(kuò)展是很容易的。 一個(gè) Java 的 例如,目前的語法:
能有效地為 把
現(xiàn)有的覆蓋(override)
應(yīng)在語言中加入更多的功能以支持線程間的相互通信。目前,
讀寫鎖的概念應(yīng)內(nèi)置到 Java 編程語言中。讀寫器鎖在“ Taming Java Threads ”(和其它地方)中有詳細(xì)討論,概括地說:一個(gè)讀寫鎖支持多個(gè)線程同時(shí)訪問一個(gè)對(duì)象,但是在同一時(shí)刻只有一個(gè)線程可以修改此對(duì)象,并且在訪問進(jìn)行時(shí)不能修改。讀寫鎖的語法可以借用
對(duì)于一個(gè)對(duì)象,應(yīng)該只有在 如果讀和寫線程都在等待,缺省情況下,讀線程會(huì)首先進(jìn)行。但是,可以使用
訪問部分創(chuàng)建的對(duì)象應(yīng)是非法的 當(dāng)前情況下,JLS 允許訪問部分創(chuàng)建的對(duì)象。例如,在一個(gè)構(gòu)造函數(shù)中創(chuàng)建的線程可以訪問正被創(chuàng)建的對(duì)象,既使此對(duì)象沒有完全被創(chuàng)建。下面代碼的結(jié)果無法確定:
設(shè)置 對(duì)此問題的一個(gè)解決方法是,在構(gòu)造函數(shù)沒有返回之前,對(duì)于在此構(gòu)造函數(shù)中創(chuàng)建的線程,既使它的優(yōu)先級(jí)比調(diào)用 這就是說,在構(gòu)造函數(shù)返回之前, 另外,Java 編程語言應(yīng)可允許構(gòu)造函數(shù)的同步。換句話說,下面的代碼(在當(dāng)前情況下是非法的)會(huì)象預(yù)期的那樣工作:
我認(rèn)為第一種方法比第二種更簡潔,但實(shí)現(xiàn)起來更為困難。
volatile 關(guān)鍵字應(yīng)象預(yù)期的那樣工作 JLS 要求保留對(duì)于 volatile 操作的請(qǐng)求。大多數(shù) Java 虛擬機(jī)都簡單地忽略了這部分內(nèi)容,這是不應(yīng)該的。在多處理器的情況下,許多主機(jī)都出現(xiàn)了這種問題,但是它本應(yīng)由 JLS 加以解決的。如果您對(duì)這方面感興趣,馬里蘭大學(xué)的 Bill Pugh 正在致力于這項(xiàng)工作(請(qǐng)參閱 參考資料)。
如果缺少良好的訪問控制,會(huì)使線程編程非常困難。大多數(shù)情況下,如果能保證線程只從同步子系統(tǒng)中調(diào)用,不必考慮線程安全(threadsafe)問題。我建議對(duì) Java 編程語言的訪問權(quán)限概念做如下限制;
由于對(duì)不變對(duì)象的訪問不需要同步,所以在多線程條件下,不變的概念(一個(gè)對(duì)象的值在創(chuàng)建后不可更改)是無價(jià)的。Java 編程言語中,對(duì)于不變性的實(shí)現(xiàn)不夠嚴(yán)格,有兩個(gè)原因:
第一個(gè)問題可以解決,不允許線程在構(gòu)造函數(shù)中開始執(zhí)行 (或者在構(gòu)造函數(shù)返回之前不能執(zhí)行開始請(qǐng)求)。 對(duì)于第二個(gè)問題,通過限定
有了 最后,當(dāng)使用內(nèi)部類(inner class)后,在 Java 編譯器中的一個(gè)錯(cuò)誤使它無法可靠地創(chuàng)建不變對(duì)象。當(dāng)一個(gè)類有重要的內(nèi)部類時(shí)(我的代碼常有),編譯器經(jīng)常不正確地顯示下列錯(cuò)誤信息:
既使空的 final 在每個(gè)構(gòu)造函數(shù)中都有初始化,還是會(huì)出現(xiàn)這個(gè)錯(cuò)誤信息。自從在 1.1 版本中引入內(nèi)部類后,編譯器中一直有這個(gè)錯(cuò)誤。在此版本中(三年以后),這個(gè)錯(cuò)誤依然存在。現(xiàn)在,該是改正這個(gè)錯(cuò)誤的時(shí)候了。 除了訪問權(quán)限外,還有一個(gè)問題,即類級(jí)(靜態(tài))方法和實(shí)例(非靜態(tài))方法都能直接訪問類級(jí)(靜態(tài))域。這種訪問是非常危險(xiǎn)的,因?yàn)閷?shí)例方法的同步不會(huì)獲取類級(jí)的鎖,所以一個(gè)
由于
或則,編譯器應(yīng)獲得讀/寫鎖的使用:
另外一種方法是(這也是一種 理想的 方法)-- 編譯器應(yīng) 自動(dòng) 使用一個(gè)讀/寫鎖來同步訪問非不變 static 域,這樣,程序員就不必?fù)?dān)心這個(gè)問題。
當(dāng)所有的非后臺(tái)線程終止后,后臺(tái)線程都被突然結(jié)束。當(dāng)后臺(tái)線程創(chuàng)建了一些全局資源(例如一個(gè)數(shù)據(jù)庫連接或一個(gè)臨時(shí)文件),而后臺(tái)線程結(jié)束時(shí)這些資源沒有被關(guān)閉或刪除就會(huì)導(dǎo)致問題。 對(duì)于這個(gè)問題,我建議制定規(guī)則,使 Java 虛擬機(jī)在下列情況下不關(guān)閉應(yīng)用程序:
后臺(tái)線程在它執(zhí)行完
重新引入 stop() 、 suspend() 和 resume() 關(guān)鍵字 由于實(shí)用原因這也許不可行,但是我希望不要廢除 對(duì)于這個(gè)問題,可以重新定義 與這種和異常類似的處理方法帶來的實(shí)際問題是,你必需在每個(gè) 應(yīng)把
應(yīng)該能打斷任何被阻斷的操作,而不是只讓它們 還有,程序應(yīng)支持 I/O 操作的超時(shí)。所有可能出現(xiàn)阻斷操作的對(duì)象(例如 InputStream 對(duì)象)也都應(yīng)支持這種方法:
這和 Socket 類的
以上是我的建議。就像我在標(biāo)題中所說的那樣,如果我是國王...(哎)。我希望這些改變(或其它等同的方法)最終能被引入 Java 語言中。我確實(shí)認(rèn)為 Java 語言是一種偉大的編程語言;但是我也認(rèn)為 Java 的線程模型設(shè)計(jì)得還不夠完善,這是一件很可惜的事情。但是,Java 編程語言正在演變,所以還有可提高的前景。 Allen 撰寫了八本書籍,最近新出的一本討論了 Java 線程的陷阱和缺陷《 Taming Java Threads 》。他長期從事設(shè)計(jì)和編制面向?qū)ο筌浖氖铝?8 年的 C++ 編程工作后,Allen 在 1996 年由 C++ 轉(zhuǎn)向 Java。他現(xiàn)在視 C++ 為一個(gè)噩夢(mèng),其可怕的經(jīng)歷正被逐漸淡忘。他從 1982 年起就自己和為加利弗尼亞大學(xué)伯克利分校教授計(jì)算機(jī)編程(首先是 C,然后是 C++ 和 MFC,現(xiàn)在是面向?qū)ο笤O(shè)計(jì)和 Java)。 Allen 也提供 Java 和面向?qū)ο笤O(shè)計(jì)方面的公眾課程和私授 (in-house) 課程。他還提供面向?qū)ο笤O(shè)計(jì)的咨詢并承包 Java 編程項(xiàng)目。請(qǐng)通過此 Web 站點(diǎn)和 Allen 取得聯(lián)系并獲取信息: www.holub.com。
|
地震讓大伙知道:居安思危,才是生存之道。
