代碼整潔之道(附錄)_第1頁
代碼整潔之道(附錄)_第2頁
代碼整潔之道(附錄)_第3頁
代碼整潔之道(附錄)_第4頁
代碼整潔之道(附錄)_第5頁
已閱讀5頁,還剩97頁未讀, 繼續(xù)免費閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進行舉報或認領(lǐng)

文檔簡介

代碼整潔之道附錄目錄\h附錄A并發(fā)編程II\hA.1客戶端/服務(wù)器的例子\hA.1.1服務(wù)器\hA.1.2添加線程代碼\hA.1.3觀察服務(wù)器端\hA.1.4小結(jié)\hA.2執(zhí)行的可能路徑\hA.2.1路徑數(shù)量\hA.2.2深入挖掘\hA.2.3小結(jié)\hA.3了解類庫\hA.3.1Executor框架\hA.3.2非鎖定的解決方案\hA.3.3非線程安全類\hA.4方法之間的依賴可能破壞并發(fā)代碼\hA.4.1容忍錯誤\hA.4.2基于客戶代碼的鎖定\hA.4.3基于服務(wù)端的鎖定\hA.5提升吞吐量\hA.5.1單線程條件下的吞吐量\hA.5.2多線程條件下的吞吐量\hA.6死鎖\hA.6.1互斥\hA.6.2上鎖及等待\hA.6.3無搶先機制\hA.6.4循環(huán)等待\hA.6.5不互斥\hA.6.6不上鎖及等待\hA.6.7滿足搶先機制\hA.6.8不做循環(huán)等待\hA.7測試多線程代碼\hA.8測試線程代碼的工具支持\hA.9小結(jié)\hA.10教程:完整代碼范例\hA.10.1客戶端/服務(wù)器非線程代碼\hA.10.2使用線程的客戶端/服務(wù)器代碼\h附錄Borg.jfree.date.SerialDate\h附錄A并發(fā)編程IIBrettL.Schuchert本附錄擴充了“并發(fā)編程”一章的內(nèi)容,由一組相互獨立的主題組成,你可以按隨意順序閱讀。為了實現(xiàn)這樣的閱讀方式,節(jié)與節(jié)之間存在一些重復(fù)內(nèi)容。\hA.1客戶端/服務(wù)器的例子想像一個簡單的客戶端/服務(wù)器應(yīng)用程序。服務(wù)器在一個套接字上等待接受來自客戶端的連接請求。客戶端連接到服務(wù)器并發(fā)送請求。\hA.1.1服務(wù)器下面是服務(wù)器應(yīng)用程序的簡化版本代碼。在后文“客戶端/服務(wù)器非多線程版本”一節(jié)中有完整的代碼。ServerSocketserverSocket=newServerSocket(8009);while(keepProcessing){try{Socketsocket=serverSocket.accept();process(socket);}catch(Exceptione){handle(e);}}這個簡單的應(yīng)用等待連接請求,處理接收到的新消息,再等待下一個客戶端請求。下面是連接到服務(wù)器的客戶端代碼:privatevoidconnectSendReceive(inti){try{Socketsocket=newSocket("localhost",PORT);MessageUtils.sendMessage(socket,Integer.toString(i));MessageUtils.getMessage(socket);socket.close();}catch(Exceptione){e.printStackTrace();}}這對客戶端/服務(wù)器程序運行得如何呢?怎樣才能正式地描述其性能?下面是斷言其性能“可接受”的測試:@Test(timeout=10000)publicvoidshouldRunInUnder10Seconds()throwsException{Thread[]threads=createThreads();startAllThreadsw(threads);waitForAllThreadsToFinish(threads);}為了讓例子夠簡單,設(shè)置過程被忽略了(見后文ClientText.java部分)。測試斷言程序應(yīng)該在10000毫秒內(nèi)完成。這是個驗證系統(tǒng)吞吐量的典型例子。系統(tǒng)應(yīng)該在10秒鐘以內(nèi)完成一組客戶端請求。只要服務(wù)器能在時限內(nèi)處理每個客戶端請求,測試就通過了。如果測試失敗會怎樣?缺少了某些事件輪詢機制,在單個線程上也沒什么可讓代碼更快的手段。使用多線程能解決問題嗎?可能會,我們先得了解什么地方耗費時間。下面是兩種可能:I/O——使用套接字、連接到數(shù)據(jù)庫、等待虛擬內(nèi)存交換等;處理器——數(shù)值計算、正則表達式處理、垃圾回收等。以上在系統(tǒng)中都會部分存在,但對于特定的操作,其中之一會起主導(dǎo)作用。如果代碼運行速度主要與處理器有關(guān),增加處理器硬件就能提升吞吐量,從而通過測試。但CPU運算周期是有上限的,因此,只是增加線程的話并不會提升受處理器限制的代碼的速度。另一方面,如果吞吐量與I/O有關(guān),則并發(fā)編程能提升運行效率。當系統(tǒng)的某個部分在等待I/O,另一部分就可以利用等待的時間處理其他事務(wù),從而更有效地利用了CPU能力。\hA.1.2添加線程代碼假定性能測試失敗了。如何才能提高吞吐量、通過性能測試呢?如果服務(wù)器的process方法與I/O有關(guān),就有個辦法讓服務(wù)器利用線程(只需要修改processMessage):voidprocess(finalSocketsocket){if(socket==null)return;RunnableclientHandler=newRunnable(){publicvoidrun(){try{Stringmessage=MessageUtils.getMessage(socket);MessageUtils.sendMessage(socket,"Processed:"+message);closeIgnoringException(socket);}catch(Exceptione){e.printStackTrace();}}};ThreadclientConnection=newThread(clientHandler);clientConnection.start();}假設(shè)修改后測試通過了\h[1]。代碼是否完整、正確了呢?\hA.1.3觀察服務(wù)器端修改了的服務(wù)器成功通過測試,只花費了一秒多鐘時間。不幸的是,這種解決手段有點一廂情愿,而且導(dǎo)致了新問題產(chǎn)生。服務(wù)器應(yīng)該創(chuàng)建多少個線程?代碼沒有設(shè)置上限,所以我們很有可能達到Java虛擬機(JVM)的限制。對于許多簡單系統(tǒng)來說這無所謂。但如果系統(tǒng)要支持公眾網(wǎng)絡(luò)上的眾多用戶呢?如果有太多用戶同時連接,系統(tǒng)就有可能掛掉。不過先把性能問題放到一邊吧。這種手段還有整潔性和結(jié)構(gòu)上的問題。服務(wù)器代碼有多少種權(quán)責呢?套接字連接管理;客戶端處理;線程策略;服務(wù)器關(guān)閉策略。這些權(quán)責不幸全在process函數(shù)中。而且,代碼跨越多個抽象層級。所以,即便process函數(shù)這么短小,還是需要再加以切分。服務(wù)器有幾個修改的原因,所以它違反了單一權(quán)責原則。要保持并發(fā)系統(tǒng)整潔,應(yīng)該將線程管理代碼約束于少數(shù)幾處控制良好的地方。而且,管理線程的代碼只應(yīng)該做管理線程的事。為什么?即便無需同時考慮其他非多線程代碼,跟蹤并發(fā)問題都已經(jīng)足夠困難了。如果為上述每個權(quán)責(包括線程管理權(quán)責在內(nèi))創(chuàng)建單獨的類,當改動線程管理策略時,就會對整個代碼產(chǎn)生較小影響,不至于污染其他權(quán)責。這樣一來,也能在不擔心線程問題的前提下測試所有其他權(quán)責。下面是修改過的版本:publicvoidrun(){while(keepProcessing){try{ClientConnectionclientConnection=connectionManager.awaitClient();ClientRequestProcessorrequestProcessor=newClientRequestProcessor(clientConnection);clientScheduler.schedule(requestProcessor);}catch(Exceptione){e.printStackTrace();}}connectionManager.shutdown();}所有與線程相關(guān)的東西都放到了clientScheduler里面。如果出現(xiàn)并發(fā)問題,只要看這個地方就好了:publicinterfaceClientScheduler{voidschedule(ClientRequestProcessorrequestProcessor);}并發(fā)策略易于實現(xiàn):publicclassThreadPerRequestSchedulerimplementsClientScheduler{publicvoidschedule(finalClientRequestProcessorrequestProcessor){Runnablerunnable=newRunnable(){publicvoidrun(){requestPcess();}};Threadthread=newThread(runnable);thread.start();}}把所有線程管理隔離到一個位置,修改控制線程的方式就容易多了。例如,移植到Java5Executor框架就只需要編寫一個新類并插進來即可(如代碼清單A-1所示)。代碼清單A-1ExecutorClientScheduler.javaimportjava.util.concurrent.Executor;importjava.util.concurrent.Executors;publicclassExecutorClientSchedulerimplementsClientScheduler{Executorexecutor;publicExecutorClientScheduler(intavailableThreads){executor=Executors.newFixedThreadPool(availableThreads);}publicvoidschedule(finalClientRequestProcessorrequestProcessor){Runnablerunnable=newRunnable(){publicvoidrun(){requestPcess();}};executor.execute(runnable);}}\hA.1.4小結(jié)本例介紹的并發(fā)編程,演示了一種提高系統(tǒng)吞吐量的方法,以及一種通過測試框架驗證吞吐量的方法。將全部并發(fā)代碼放到少數(shù)類中,是應(yīng)用單一權(quán)責原則的范例。對于并發(fā)編程,因其復(fù)雜性,這一點尤其重要。\hA.2執(zhí)行的可能路徑復(fù)查沒有循環(huán)或條件分支的單行Java方法incrementValue:publicclassIdGenerator{intlastIdUsed;publicintincrementValue(){return++lastIdUsed;}}忽略整數(shù)溢出的情形,假定只有單個線程能訪問IdGenerator的單個實體。這種情況下,只有一種執(zhí)行路徑和一個確定的結(jié)果:返回值等于lastIdUsed的值,兩者都比調(diào)用方法前大1。如果使用兩個線程、不修改方法的話會發(fā)生什么?如果每個線程都調(diào)用一次incrementValue,可能得到什么結(jié)果呢?有多少種可能執(zhí)行路徑?首先來看結(jié)果(假定lastIdUsed初始值為93):線程1得到94,線程2得到95,lastIdUsed為95;線程1得到95,線程2得到94,lastIdUsed為95;線程1得到94,線程2得到94,lastIdUsed為94。最后一個結(jié)果盡管令人吃驚,也是有可能出現(xiàn)的。要想明白為何可能出現(xiàn)這些結(jié)果,就需要理解可能執(zhí)行路徑的數(shù)量以及Java虛擬機是如何執(zhí)行這些路徑的。\hA.2.1路徑數(shù)量為了算出可能執(zhí)行路徑的數(shù)量,我們從生成的字節(jié)碼開始研究。那行Java代碼(return++lastIdUsed;)變成了8個字節(jié)碼指令。兩個線程有可能交錯執(zhí)行這8個指令,就像莊家在洗牌時交錯牌張一樣\h[2]。即便每只手上只有8張牌,洗牌得到的結(jié)果數(shù)量也很可觀。對于指令系列中有N個指令和T個線程、沒有循環(huán)或條件分支的簡單情況,總的可能執(zhí)行路徑數(shù)量等于計算可能執(zhí)行次序以下摘自鮑勃大叔給Brett的一封電子郵件:對于N步指令和T個線程,總共有T*N個步驟。在執(zhí)行每步指令之前,會有在T個線程中選擇其一的環(huán)境開關(guān)。因而每條路徑都能以一個數(shù)字字符串的形式來表示該環(huán)境開關(guān)。對于步驟A、B及線程1和2,可能有6條可能路徑:1122、1212、1221、2112、2121和2211?;蛘咭灾噶畈襟E表示為A1B1A2B2、A1A2B1B2、A1A2B2B1、A2A1B1B2、A2A1B2B1及A2B2A1B1。對于三個線程,執(zhí)行序列就是112233、112323、113223、113232、112233、121233、121323、121332、123132、123123……這些字符串的特征之一是每個T總會出現(xiàn)N次。所以字符串111111是無效的,因為里面有6個1,而2和3則未出現(xiàn)過。所以要排列組合N1、N2……直至NT。這其實就是N*T對應(yīng)N*T的排列,即(N*T)!,但要剔除重復(fù)的情形。所以,巧妙之處就在于計算重復(fù)次數(shù)并從(N*T)!中剔除掉。對于兩步指令和兩個線程,有多少重復(fù)呢?每個四位數(shù)字符串中都有兩個1和兩個2。每個這種配對都可以在不影響字符串意義的前提下調(diào)換??梢酝瑫r調(diào)換全部1和2,也可以都不調(diào)換。所以每個字符串就有四種同構(gòu)形態(tài),即存在3次重復(fù)。所以四分之三的路徑是重復(fù)的;而四分之一的排列則不重復(fù)。4!*.25=6。這樣計算看來可行。有多少重復(fù)呢?對于N=1且T=2的情形,我可以調(diào)換1,調(diào)換2,或兩者都調(diào)換。對于N=2且T=3的情形,我可以調(diào)換1、2、3,1和2,1和3,或2和3。調(diào)換只是N的排列組合罷了。設(shè)有N的P種排列組合。排列組合的方式總共有P**T種。所以可能的同構(gòu)形態(tài)數(shù)量為N!**T。路徑的數(shù)量就是(T*N)!/(N!**T)。對于T=2且N=2的情況,結(jié)果就是6(即24/4)。對于N=2且T=3,結(jié)果是720/8=90。對于N=3且T=3,結(jié)果是9!/6^3=1680。對于一行Java代碼(等同于8行字節(jié)碼)和兩個線程的簡單情況,可能執(zhí)行路徑的總數(shù)量就是12870。如果lastIdUsed的類型為long,每次讀/寫操作都變成了兩次操作,而可能的次序高達2704156種。如果改動一下該方法會怎樣?publicsynchronizedvoidincrementValue(){++lastIdUsed;}這樣一來,對于兩個線程的情況,可能執(zhí)行路徑的數(shù)量就是2,即N!。\hA.2.2深入挖掘兩個線程都調(diào)用方法一次(在添加synchronize之前)、得到同一結(jié)果數(shù)字的驚異結(jié)果又怎樣呢?怎么可能出現(xiàn)這種情況?一樣一樣來。什么是原子操作?可以把原子操作定義為不可中斷的操作。例如,在下列代碼的第5行,0被賦值給lastid,就是一個原子操作。因為依據(jù)Java內(nèi)存模型,32位值的賦值操作是不可中斷的。01:publicclassExample{02:intlastId;03:04:publicvoidresetId(){05:value=0;06:}07:08:publicintgetNextId(){09:++value;10:}11:}如果把lastId的類型從int改為long會怎樣?第5行還是原子操作嗎?如果不考慮JVM規(guī)約,則有可能根據(jù)處理器不同而不同。不過,根據(jù)JVM規(guī)約,64位值的賦值需要兩次32位賦值。這意味著在第一次和第二次32位賦值之間,其他線程可能插進來,修改其中一個值。第9行的前遞增操作符++又怎樣呢?前遞增操作符可以被中斷,所以它不是原子的。為了理解這點,仔細復(fù)查一下這些方法的字節(jié)碼吧。在更進一步之前,有三個重要的定義:框架——每個方法調(diào)用都需要一個框架。該框架包括返回地址、傳入方法的參數(shù),以及方法中定義的本地變量。這是定義一個調(diào)用堆棧的標準技術(shù),現(xiàn)代編程語言用來實現(xiàn)基本函數(shù)/方法調(diào)用和遞歸調(diào)用;本地變量——方法作用范圍內(nèi)定義的每個變量。所有非靜態(tài)方法至少有一個變量this,代表當前對象,即接收導(dǎo)致方法調(diào)用的(當前線程內(nèi))大多數(shù)最新消息的對象;運算對象?!狫ava虛擬機中的許多指令都有參數(shù)。運算對象棧是放置參數(shù)的地方。堆棧是個標準的后入先出(LIFO)數(shù)據(jù)結(jié)構(gòu)。下面是restId()的字節(jié)碼,如表A-1所示。表A-1restId()的字節(jié)碼這三個指令確保是原子的,因為盡管執(zhí)行它們的線程可能在其中任何一個指令后被打斷,但PUTFIELD指令(堆棧頂部的常量值0和頂端之下的this引用及其字段值)的信息并不能為其他線程所觸及。所以,當賦值操作發(fā)生時,值0一定將存儲到字段值中。該操作是原子的。操作對象都處理對于方法而言是本地的信息,故在多個線程之間并無沖突。所以,如果這三個指令由10個線程執(zhí)行,就會有4.38679733629e+24種可能的執(zhí)行次序。不過,只會有一種可能的結(jié)果,所以執(zhí)行次序不同無關(guān)緊要。對于本例中的long常量,總是有同一種運算結(jié)果。為什么?因為10個線程的賦值操作都是針對一個常量的。即便它們互相干涉,結(jié)果也是一樣。方法getNextId中的++操作就會有問題了。假定lastId在方法開始時的值為42.下面是新方法的字節(jié)碼,如表A-2所示。表A-2新方法的字節(jié)碼設(shè)想第一個線程完成了前三個操作,直到執(zhí)行完GETFIELD,然后被打斷。第二個線程接手并完成整個方法調(diào)用,lastId的值遞增1;得到的值為43。第一個線程再從中斷處繼續(xù)執(zhí)行;操作對象棧中的值還是42,因為那就是該線程執(zhí)行GETFIELD時的lastId值。線程給lastId加1,得到43,存儲這個結(jié)果。第一個線程也得到了值43。結(jié)果就是其中一個遞增操作丟失了,因為第一個線程在被第二個線程打斷后又踏入了第二個線程中。將getNextId()方法修改為同步方法就能修正這個問題。\hA.2.3小結(jié)理解線程之間如何互相干涉,并不一定要精通字節(jié)碼。如果你能看明白這個例子,它應(yīng)該已經(jīng)展示了多個線程之間互相干涉的可能性,這已經(jīng)足夠了。這個小例子說明,有必要盡量理解內(nèi)存模型,明白什么是安全的,什么是不安全的。有一種普遍的誤解,認為++(前遞增或后遞增)操作符是原子的,其實并非如此。你必須知道:什么地方有共享對象/值;哪些代碼會導(dǎo)致并發(fā)讀/寫問題;如何防止這種并發(fā)問題發(fā)生。\hA.3了解類庫\hA.3.1Executor框架如前文ExecutorClientScheduler.java所演示的那樣,Java5中引入的Executor框架支持利用線程池進行復(fù)雜的執(zhí)行。那就是java.util.concurrent包中的一個類。如果在創(chuàng)建線程時沒有使用線程池或自行編寫線程池,可以考慮使用Executor。它能讓代碼更整潔,易于理解,且更加短小。Executor框架將把線程放到池中,自動調(diào)整其大小,并在必要時重建線程。它還支持future,一種通用的并發(fā)編程構(gòu)造。Executor能與實現(xiàn)了Runnable的類協(xié)同工作,也能與實現(xiàn)了Callable接口的類協(xié)同工作。Callback看來就像是Runnable,但它能返回一個結(jié)果,那在多線程解決方案中是普遍的需求。當代碼需要執(zhí)行多個相互獨立的操作并等待這些操作結(jié)束時,future剛好就手:publicStringprocessRequest(Stringmessage)throwsException{Callable<String>makeExternalCall=newCallable<String>(){publicStringcall()throwsException{Stringresult="";//makeexternalrequestreturnresult;}};Future<String>result=executorService.submit(makeExternalCall);StringpartialResult=doSomeLocalProcessing();returnresult.get()+partialResult;}在本例中,方法開始執(zhí)行makeExternalCall對象。然后該方法繼續(xù)其他操作。最后一行代碼調(diào)用result.get(),在future代碼執(zhí)行完成前,這個操作是鎖定的。\hA.3.2非鎖定的解決方案Java5虛擬機利用了現(xiàn)代處理器支持可靠、非鎖定更新的設(shè)計優(yōu)點。例如,考慮某個使用同步(從而也是鎖定的)來提供線程安全地更新一個值的類:publicclassObjectWithValue{privateintvalue;publicvoidsynchronizedincrementValue(){++value;}publicintgetValue(){returnvalue;}}Java5有一系列用于此類情況的新類,例如AtomicBoolean、AtomicInteger和AtomicReference等;還有另外一些。我們可以重寫上面的代碼,使用非鎖定的手段,如下所示:publicclassObjectWithValue{privateAtomicIntegervalue=newAtomicInteger(0);publicvoidincrementValue(){value.incrementAndGet();}publicintgetValue(){returnvalue.get();}}即便使用了對象而非直接操作,使用了incrementAndGet()這樣的信息發(fā)送方式而非++操作,這個類的性能還是幾乎總能勝過上一版本。在某些情況下只會快一點點,但較慢的情形卻幾乎不存在。怎么會這樣?現(xiàn)代處理器擁有一種通常稱為比較交換(CompareandSwap,CAS)的操作。這種操作類似于數(shù)據(jù)庫中的樂觀鎖定,而其同步版本則類似于保守鎖定。關(guān)鍵字synchronized總是要求上鎖,即便第二個線程并不更新同一值時也如此。盡管這種固有鎖的性能一直在提升,但仍然代價昂貴。非上鎖的版本假定多個線程通常并不頻繁修改同一個值,導(dǎo)致問題產(chǎn)生。它高效地偵測這種情形是否發(fā)生,并不斷嘗試,直至更新成功。這種偵測行為幾乎總是比上鎖來得劃算,在爭用激烈的情況下也是如此。虛擬機如何實現(xiàn)這種機制?CAS的操作是原子的。邏輯上,CAS操作看起來像這樣:intvariableBeingSet;voidsimulateNonBlockingSet(intnewValue){intcurrentValue;do{currentValue=variableBeingSet}while(currentValue!=compareAndSwap(currentValue,newValue));}intsynchronizedcompareAndSwap(intcurrentValue,intnewValue){if(variableBeingSet==currentValue){variableBeingSet=newValue;returncurrentValue;}returnvariableBeingSet;}當某個方法試圖更新一個共享變量,CAS操作就會驗證要賦值的變量是否保有上一次的已知值。如果是,就修改變量值。如果不是,則不會碰變量,因為另一個線程正在試圖更新變量值。要更新數(shù)據(jù)的方法(通過CAS操作)查看是否修改并持續(xù)嘗試。\hA.3.3非線程安全類有些類天生不是線程安全的。下面是幾個例子:數(shù)據(jù)庫連接java.util中的容器Servlet注意,有些群集類擁有一些線程安全的方法。不過,涉及調(diào)用多個方法的操作都不是線程安全的。例如,如果因為HashTable中已經(jīng)有某物而不打算替換它,可能會寫出以下代碼:if(!hashTable.containsKey(someKey)){hashTable.put(someKey,newSomeValue());}單個方法是線程安全的。不過,另一個線程卻可能在containsKey和put調(diào)用之間塞進一個值。有幾種修正這個問題的手段。先鎖定HashTable,確定其他使用者都做了基于客戶端的鎖定:synchronized(map){if(!map.conainsKey(key))map.put(key,value);}用其對象包裝HashTable,并使用不同的API——利用ADAPTER模式做基于服務(wù)端的鎖定:publicclassWrappedHashtable<K,V>{privateMap<K,V>map=newHashtable<K,V>();publicsynchronizedvoidputIfAbsent(Kkey,Vvalue){if(map.containsKey(key))map.put(key,value);}}采用線程安全的群集:ConcurrentHashMap<Integer,String>map=newConcurrentHashMap<Integer,String>();map.putIfAbsent(key,value);在java.util.concurrent中的群集都有putIfAbsent()之類提供這種操作的方法。\hA.4方法之間的依賴可能破壞并發(fā)代碼以下是一個有關(guān)在方法間引入依賴的小例子:publicclassIntegerIteratorimplementsIterator<Integer>privateIntegernextValue=0;publicsynchronizedbooleanhasNext(){returnnextValue<100000;}publicsynchronizedIntegernext(){if(nextValue==100000)thrownewIteratorPastEndException();returnnextValue++;}publicsynchronizedIntegergetNextValue(){returnnextValue;}}下面是使用IntegerIterator的代碼:IntegerIteratoriterator=newIntegerIterator();while(iterator.hasNext()){intnextValue=iterator.next();//dosomethingwithnextValue}如果只有一個線程執(zhí)行這段代碼,不會有什么問題。但如果有兩個線程抱著每個線程都處理它獲得的值、但列表中的每個元素都只被處理一次的意圖,嘗試共享IntegerIterator的單個實體,會發(fā)生什么事?多數(shù)時候什么也不會發(fā)生;線程開心地共享著列表,處理從迭代器獲取的元素,在迭代器完成執(zhí)行時停下。然而,在迭代的末尾,兩個線程也有少量可能互相干涉,導(dǎo)致其中一個超出迭代器末尾,拋出異常。問題在這里。線程1調(diào)用hasNext()方法,該方法返回true。線程1占先,然后線程2也調(diào)用這個方法,同樣返回true。線程2接著調(diào)用next(),該方法如期返回一個值,但副作用是之后再調(diào)用hasNext()就會返回false。線程1繼續(xù)執(zhí)行,以為hasNext()還是true,然后調(diào)用next()。即便單個方法是同步的,客戶端還是使用了兩個方法。這的確是個問題,也是并發(fā)代碼中此類問題的典型例子。在這個特殊例子中,問題尤其隱蔽,因為只有在迭代器最后一次迭代時發(fā)生才會導(dǎo)致錯誤。如果線程剛好在那個點中斷,其中一個線程就可能超出迭代器末尾。這類錯誤往往在系統(tǒng)部署之后很久才發(fā)生,而且很難追蹤。出現(xiàn)錯誤時,你有3種做法。容忍錯誤;修改客戶代碼解決問題:基于客戶代碼的鎖定;修改服務(wù)端代碼解決問題,同時也修改了客戶代碼:基于服務(wù)端的鎖定。\hA.4.1容忍錯誤有時,可以通過一些設(shè)置讓錯誤不會導(dǎo)致?lián)p害。例如,上述客戶代碼可以捕捉并清理異常。坦白地說,這有點草草從事,就像是半夜重啟解決內(nèi)存泄露問題一樣。\hA.4.2基于客戶代碼的鎖定要讓IntegerIterator在多線程情況下正確運行,對客戶代碼做如下修改:IntegerIteratoriterator=newIntegerIterator();while(true){intnextValue;synchronized(iterator){if(!iterator.hasNext())break;nextValue=iterator.next();}doSometingWith(nextValue);}每個客戶端都通過synchronized關(guān)鍵字引入一個鎖。這種重復(fù)違反了DRY原則,但如果代碼使用非線程安全的第三方工具,可能必須這樣做。這種策略有風險,因為使用服務(wù)端的程序員都得記住在使用前上鎖、用過后解鎖。許多(許多?。┠昵?,我遇到過一個在共享資源上應(yīng)用基于客戶代碼鎖定的系統(tǒng)。代碼中有幾百處用到這個資源的地方。有位可憐的程序員忘記在其中一處做資源鎖定。該系統(tǒng)是個多終端分時系統(tǒng),為Local705卡車司機聯(lián)盟運行會計軟件。計算機放在距Local705總部50英里(約84.65km)以北的一間鑲有高于地面的地板、環(huán)境可控的機房中??偛坑袔资粩?shù)據(jù)錄入員,往終端輸入記錄。終端使用電話專線和600bit/s的半雙工調(diào)制解調(diào)器連接到計算機。(這可是很久很久以前的事了。)每天大概都會有一臺終端毫無理由地“死鎖”。死鎖也不限定在某些終端或特定時間。就像是有人擲骰子選擇死鎖的時機和終端一般。有時,會有幾臺終端死鎖。有時,好幾天都不出現(xiàn)死鎖情況。剛開始,唯一的解決手段就是重啟。但協(xié)同起來很不便。我們得打電話給總部,讓大家都完成在終端上的工作。然后我們才能關(guān)機、重啟。如果有人在做要花上一兩個小時才能做完的事,被鎖定的終端就只能一直等著。經(jīng)過幾個星期的調(diào)試,我們發(fā)現(xiàn),原因在于一個指針不同步的環(huán)形緩沖區(qū)計數(shù)器。該緩沖區(qū)控制向終端的輸出。指針值說明緩沖區(qū)是空的,但計數(shù)器卻指出緩沖區(qū)是滿的。因為緩沖區(qū)是空的,就沒什么可顯示;但因為緩沖區(qū)也是滿的,也就無法向其中加入可在屏幕上顯示的內(nèi)容。我們知道了終端為何會死鎖,但卻不知道為什么環(huán)形緩沖區(qū)會不同步。我們用了點手段發(fā)現(xiàn)問題所在。當時程序能夠讀取計算機的前面板開關(guān)狀態(tài)(這可是很久很久以前的事了)。我們寫了個陷阱程序,偵測這些開關(guān)何時被撥動,然后查找既空又滿的環(huán)形緩沖區(qū)。如果找到,就重置該緩沖區(qū)為空。烏拉!鎖定的終端又重新開始顯示了。這樣,在終端鎖定時就不必重啟系統(tǒng)了??蛻糁恍枰螂娫捀嬖V我們出現(xiàn)死鎖,我們就徑直走到機房,撥動一下開關(guān)即可。當然,有時他們會在周末加班,但是我們可不加班。所以我們又在計劃列表中添加了一個函數(shù),每分鐘檢查一次全部環(huán)形緩沖區(qū),重置既空又滿的緩沖區(qū)。在客戶打電話之前,顯示就已經(jīng)恢復(fù)正常了。在發(fā)現(xiàn)問題原因之前,我們花了好幾個星期查看一頁又一頁的單片機匯編語言代碼。我們已經(jīng)完成計算,算出死鎖的頻率是周期性的,而且其中有一處未受保護的環(huán)形緩沖區(qū)使用。所以,剩下的任務(wù)就是找出那個錯誤的用法。不幸這是多年以前的事,那時既沒有搜索工具,也沒有交叉引用或任何其他自動化幫助手段。我們只能細查代碼清單。在芝加哥1971年的寒冬,我學到了重要的一課?;诳蛻舸a的鎖定實在不可靠。\hA.4.3基于服務(wù)端的鎖定按照以下方式修改IntegerIterator也能消除重復(fù):publicclassIntegerIteratorServerLocked{privateIntegernextValue=0;publicsynchronizedIntegergetNextOrNull(){if(nextValue<100000)returnnextValue++;elsereturnnull;}}客戶代碼也要修改:while(true){IntegernextValue=iterator.getNextOrNull();if(next==null)break;//dosomethingwithnextValue}在這種情形下,我們實際上是修改了類的API,使其能適應(yīng)多線程\h[3]??蛻舳诵枰鰊ull檢查,而不是檢查hasNext()。通常你應(yīng)該選用基于服務(wù)端的鎖定,因為:它減少了重復(fù)代碼——采用基于客戶代碼的鎖定,每個客戶端都要正確鎖定服務(wù)端。把鎖定代碼放到服務(wù)端,客戶端就能自由使用對象,不必費心編寫額外的鎖定代碼;它提升了性能——在單線程部署中,可以用非多線程安全服務(wù)端代碼替代線程安全客戶端,從而省去花銷;它減少了出錯的可能性——只會有一個程序員忘記上鎖;它執(zhí)行了單一策略——該策略只在服務(wù)端這一處地方實施,而不是在許多地方(每個客戶端)實施;它縮減了共享變量的作用范圍——客戶端不必關(guān)心它們或它們是如何鎖定的。一切都隱藏在服務(wù)端。如果出錯,要偵查的范圍就小多了。如果你無法修改服務(wù)端代碼又該如何?使用ADAPTER模式修改API,添加鎖定;publicclassThreadSafeIntegerIterator{privateIntegerIteratoriterator=newIntegerIterator();publicsynchronizedIntegergetNextOrNull(){if(iterator.hasNext())returniterator.next();returnnull;}}更好的方法是使用線程安全的群集和擴展接口。\hA.5提升吞吐量假設(shè)我們打算連接上網(wǎng),從一個URL列表中讀取一組頁面的內(nèi)容。讀到一個頁面時,解析該頁面并得到一些統(tǒng)計結(jié)果。讀完所有頁面后,打印出一份提要報表。下面的類返回給定URL的頁面內(nèi)容:publicclassPageReader{//...publicStringgetPageFor(Stringurl){HttpMethodmethod=newGetMethod(url);try{httpClient.executeMethod(method);Stringresponse=method.getResponseBodyAsString();returnresponse;}catch(Exceptione){handle(e);}finally{method.releaseConnection();}}}下一個類是給出URL迭代器中每個頁面的內(nèi)容的迭代器:publicclassPageIterator{privatePageReaderreader;privateURLIteratorurls;publicPageIterator(PageReaderreader,URLIteratorurls){this.urls=urls;this.reader=reader;}publicsynchronizedStringgetNextPageOrNull(){if(urls.hasNext())getPageFor(urls.next());elsereturnnull;}publicStringgetPageFor(Stringurl){returnreader.getPageFor(url);}}PageIterator的一個實體可為多個不同線程共享,每個線程使用自己的PageReader實體讀取并解析從迭代器中得到的頁面。注意,我們把synchronized代碼塊的數(shù)量限制在小范圍之內(nèi)。它只包括深處于PageIterator內(nèi)部的臨界區(qū)。最好是盡可能少地使用同步。\hA.5.1單線程條件下的吞吐量來做個簡單計算。鑒于討論的目的,假定:獲取一個頁面的I/O時間(平均)是1s;解析一個頁面的處理時間(平均)是0.5s;I/O操作不耗費處理器能力,而解析頁面耗費100%處理器能力。對于單個線程要處理的N個頁面,總的執(zhí)行時間為1.5s*N。圖A-1顯示了13個頁面或大概19.5s的快照。圖A-1單線程\hA.5.2多線程條件下的吞吐量如果能夠以任意次序獲得頁面并獨立處理頁面,就有可能利用多線程提升吞吐量。如果我們使用三個線程會如何?在同一時間內(nèi)能獲取多少個頁面呢?如你在圖A-2中所見,多線程方案中與處理器能力有關(guān)的頁面解析操作可以和與I/O有關(guān)的頁面讀取操作疊加進行。在理想狀態(tài)下,這意味著處理器力盡其用。每個耗時一秒鐘的頁面讀取操作都與兩次解析操作疊加進行。這樣,我們就能在每秒鐘內(nèi)處理兩個頁面,即三倍于單線程方案的吞吐量。圖A-2三個并發(fā)線程\hA.6死鎖想象一個擁有兩個有限共享資源池的Web應(yīng)用程序。一個用于本地臨時工作存儲的數(shù)據(jù)庫連接池;一個用于連接到主存儲庫的MQ池。假定該應(yīng)用中有兩個操作:創(chuàng)建和更新。創(chuàng)建——獲取到主存儲庫和數(shù)據(jù)庫的連接。與主存儲庫協(xié)調(diào),并把工作保存到本地臨時工作數(shù)據(jù)庫;更新——先獲取到數(shù)據(jù)庫的連接,再獲取到主存儲庫的連接。從臨時工作數(shù)據(jù)庫中讀取數(shù)據(jù),再發(fā)送給主存儲庫。如果用戶數(shù)量多于池的大小會怎樣?假設(shè)每個池中能容納10個資源。有10個用戶嘗試創(chuàng)建,獲取了10個數(shù)據(jù)庫連接,每個線程在獲取到數(shù)據(jù)庫連接之后、獲取到主存儲庫連接之前都被打斷;有10個用戶嘗試更新,獲取了10個主存儲庫連接,每個線程在獲取到主存儲庫連接之后、獲取到數(shù)據(jù)庫連接之前都會被打斷;現(xiàn)在那10個“創(chuàng)建”線程必須等待獲取主存儲庫連接,但那10個“更新”線程必須等待獲取數(shù)據(jù)庫連接;死鎖。系統(tǒng)永遠無法恢復(fù)。這聽起來不太會出現(xiàn),但誰會想要一個每隔一周就僵在那里不動的系統(tǒng)呢?誰想要調(diào)試出現(xiàn)了難以復(fù)現(xiàn)的癥狀的系統(tǒng)呢?這種問題突然發(fā)生,然后得花上好幾個星期才能解決。典型的“解決方案”是加入調(diào)試語句,發(fā)現(xiàn)問題。當然,調(diào)試語句對代碼的修改足以令死鎖在不同情況下發(fā)生,而且要幾個月后才會再出現(xiàn)\h[4]。要真正地解決死鎖問題,我們需要理解死鎖的原因。死鎖的發(fā)生需要4個條件:互斥;上鎖及等待;無搶先機制;循環(huán)等待。\hA.6.1互斥當多個線程需要使用同一資源,且這些資源滿足下列條件時,互斥就會發(fā)生。無法在同一時間為多個線程所用;數(shù)量上有限制。這種資源的常見例子是數(shù)據(jù)庫連接、打開后用于寫入的文件、記錄鎖或是信號量。\hA.6.2上鎖及等待當某個線程獲取一個資源,在獲取到其他全部所需資源并完成其工作之前,不會釋放這個資源。\hA.6.3無搶先機制線程無法從其他線程處奪取資源。一個線程持有資源時,其他線程獲得這個資源的唯一手段就是等待該線程釋放資源。\hA.6.4循環(huán)等待這也被稱為“死命擁抱”。想象兩個線程,T1和T2,還有兩個資源,R1和R2。T1擁有R1,T2擁有R2。T1需要R2,T2需要R1。如此就出現(xiàn)了如圖A-3所示的情形。這4種條件都是死鎖所必需的。只要其中一個不滿足,死鎖就不會發(fā)生。圖A-3循環(huán)等待\hA.6.5不互斥避免死鎖的一種策略是規(guī)避互斥條件。你可以:使用允許同時使用的資源,如AtomicInteger;增加資源數(shù)量,使其等于或大于競爭線程的數(shù)量;在獲取資源之前,檢查是否可用。不幸的是,多數(shù)資源都有上限,且不能同時使用。而且第二個資源的標識也常常要依據(jù)對第一個資源的操作結(jié)果來判斷。不過別喪氣,還有3個其他條件呢。\hA.6.6不上鎖及等待如果拒絕等待,就能消除死鎖。在獲得資源之前檢查資源,如果遇到某個繁忙資源,就釋放所有資源,重新來過。這種手段帶來幾個潛在問題:線程饑餓——某個線程一直無法獲得它所需的資源(它可能需要某種很少能同時獲得的資源組合);活鎖——幾個線程可能會前后相連地要求獲得某個資源,然后再釋放一個資源,如此循環(huán)。這在單純的CPU任務(wù)排列算法中尤其有可能出現(xiàn)(想想嵌入式設(shè)備或單純的手寫線程平衡算法)。二者都能導(dǎo)致較差的吞吐量。第一個的結(jié)果是CPU利用率低,第二個的結(jié)果是較高但無用的CPU利用率。盡管這種策略聽起來沒效率,但也好過沒有。至少,如果其他方案不奏效,這種手段幾乎總可以用上。\hA.6.7滿足搶先機制避免死鎖的另一策略是允許線程從其他線程上奪取資源。這通常利用一種簡單的請求機制來實現(xiàn)。當線程發(fā)現(xiàn)資源繁忙,就要求其擁有者釋放之。如果擁有者還在等待其他資源,就釋放全部資源并重新來過。這和上一種手段相似,但好處是允許線程等待資源。這減少了線程重新啟動的次數(shù)。不過,管理所有請求可要花點心思。\hA.6.8不做循環(huán)等待這是避免死鎖的最常用手段。對于多數(shù)系統(tǒng),它只要求一個為各方認同的約定。在上面的例子中線程1同時需要資源1和資源2、線程2同時需要資源2和資源1,只要強制線程1和線程2以同樣次序分配資源,循環(huán)等待就不會發(fā)生。更普遍地,如果所有線程都認同一種資源獲取次序,并按照這種次序獲取資源,死鎖就不會發(fā)生。就像其他策略一樣,這也會有問題:獲取資源的次序可能與使用資源的次序不匹配;一開始獲取的資源可能在最后才會用到。這可能導(dǎo)致資源不必要地被長時間鎖定;有時無法強求資源獲取順序。如果第二個資源的ID來自對第一個資源操作的結(jié)果,獲取次序也無從談起。有許多避免死鎖的方法。有些會導(dǎo)致饑餓,另外一些會導(dǎo)致對CPU能力的大量耗費和降低響應(yīng)率。TANSTAAFL\h[5]!將解決方案中與線程相關(guān)的部分分隔出來,再加以調(diào)整和試驗,是獲得判斷最佳策略所需的洞見的正道。\hA.7測試多線程代碼怎么才能編寫顯示以下代碼有錯的測試呢?01:publicclassClassWithThreadingProblem{02:intnextId;03:04:publicinttakeNextId(){05:returnnextId++;06:}07:}下面是對能證明上列代碼有錯的測試的描述:記住nextId的當前值;創(chuàng)建兩個線程,每個都調(diào)用takeNextId()一次;驗證nextId比開始時大2;持續(xù)運行,直至發(fā)現(xiàn)nextId只比開始時大1為止。代碼清單A-2展示了這樣一個測試:代碼清單A-2ClassWithThreadingProblemTest.java01:packageexample;02:03:importstaticorg.junit.Assert.fail;04:05:importorg.junit.Test;06:07:publicclassClassWithThreadingProblemTest{08:@Test09:publicvoidtwoThreadsShouldFailEventually()throwsException{10:finalClassWithThreadingProblemclassWithThreadingProblem=newClassWithThreadingProblem();11:12:Runnablerunnable=newRunnable(){13:publicvoidrun(){14:classWithThreadingProblem.takeNextId();15:}16:};17:18:for(inti=0;i<50000;++i){19:intstartingId=classWithThreadingProblem.lastId;20:intexpectedResult=2+startingId;21:22:Threadt1=newThread(runnable);23:Threadt2=newThread(runnable);24:t1.start();25:t2.start();26:t1.join();27:t2.join();28:29:intendingId=classWithThreadingProblem.lastId;30:31:if(endingId!=expectedResult)32:return;33:}34:35:fail("Shouldhaveexposedathreadingissuebutitdidnot.");36:}37:}表A-3代碼清單A-2的注解這個測試當然設(shè)置了滿足并發(fā)更新問題發(fā)生的條件。不過,問題發(fā)生得如此頻繁,測試也就極有可能偵測不到。實際上,要真正偵測到問題,需要將循環(huán)數(shù)量設(shè)置到100萬次以上。即便是這樣,在10個100萬次循環(huán)的執(zhí)行中,錯誤也只發(fā)生了一次。這意味著我們可能要把循環(huán)次數(shù)設(shè)置為超過億次才能獲得可靠的失敗證明。要等多久呢?即便我們調(diào)優(yōu)測試,在單臺機器上得到可靠的失敗證明,我們可能還需要用不同的值來重新設(shè)置測試,得到在其他機器、操作系統(tǒng)或不同版本的JVM上的失敗證明。而且這只是個簡單問題。如果連這個簡單問題都無法輕易獲得出錯證明,我們怎么能真正偵測復(fù)雜問題呢?我們能用什么手段來證明這個簡單錯誤呢?而且,更重要的是,我們?nèi)绾文軐懗鲎C明更復(fù)雜代碼中的錯誤的測試呢?我們怎樣才能在不知道從何處著手時知道代碼是否出錯了呢?下面是一些想法:蒙特卡洛測試。測試要靈活,便于調(diào)整。多次運行測試——在一臺測試服務(wù)器上——隨機改變調(diào)整值。如果測試失敗,代碼就有錯。確保及早編寫這些測試,好讓持續(xù)集成服務(wù)器盡快開始運行測試。另外,確認小心記錄了在何種條件下測試失敗。在每種目標部署平臺上運行測試。重復(fù)運行。持續(xù)運行。測試在不失敗的前提下運行得越久,就越能說明:-生產(chǎn)代碼正確;或;-測試不足以暴露問題。在另一臺有不同負載的機器上運行測試。能模擬生產(chǎn)環(huán)境的負載,就模擬之。即便你做了所有這些,還是不見得有很好的機會發(fā)現(xiàn)代碼中的線程問題。最陰險的問題擁有很小的截面,在十億次執(zhí)行中只會發(fā)生一次。這類錯誤是復(fù)雜系統(tǒng)的噩夢。\hA.8測試線程代碼的工具支持IBM提供了一個名為ConTest的工具\h[6]。它能對類進行裝置,令非線程安全代碼更有可能失敗。我們與IBM或開發(fā)ConTest的團隊沒有直接關(guān)系。有位同事發(fā)現(xiàn)了這個工具。在用了幾分鐘后,我們發(fā)現(xiàn)自己發(fā)現(xiàn)線程問題的能力得到了很大提升。下面是使用ConTest的簡要步驟:編寫測試和生產(chǎn)代碼,確保有專門模擬多用戶在多種負載情況下操作的測試,如上文所述;用ConTest裝置測試和生產(chǎn)代碼;運行測試。用ConTest裝置代碼后,原本千萬次循環(huán)才能暴露一個錯誤的比率提升到30次循環(huán)就能找到錯誤。以下是裝置代碼后的幾次測試運行結(jié)果值:13、23、0、54、16、14、6、69、107、49和2。顯然裝置后的類更加容易和可靠地被證明失敗。\hA.9小結(jié)本章只是在并發(fā)編程廣闊而可怕的領(lǐng)地上的短暫逗留罷了。我們只觸及了地表。我們在這里強調(diào)的,只是保持并發(fā)代碼整潔的一些規(guī)程,如果要編寫并發(fā)系統(tǒng),還有許多東西要學。建議從DougLea的大作ConcurrentProgramminginJava:DesignPrinciplesandPatterns開始\h[7]。在本章中,我們談到并發(fā)更新,還有清理及避免同步的規(guī)程。我們談到線程如何提升與I/O有關(guān)的系統(tǒng)的吞吐量,展示了獲得這種提升的整潔技術(shù)。我們談到死鎖及干凈地避免死鎖的規(guī)程。最后,我們談到通過裝置代碼暴露并發(fā)問題的策略。\hA.10教程:完整代碼范例\hA.10.1客戶端/服務(wù)器非線程代碼代碼清單A-3Server.javapackagecom.objectmentor.clientserver.nonthreaded;importjava.io.IOException;import.ServerSocket;import.Socket;import.SocketException;importcommon.MessageUtils;publicclassServerimplementsRunnable{ServerSocketserverSocket;volatilebooleankeepProcessing=true;publicServer(intport,intmillisecondsTimeout)throwsIOException{serverSocket=newServerSocket(port);serverSocket.setSoTimeout(millisecondsTimeout);}publicvoidrun(){System.out.printf("ServerStarting\n");while(keepProcessing){try{System.out.printf("acceptingclient\n");Socketsocket=serverSocket.accept();System.out.printf("gotclient\n");process(socket);}catch(Exceptione){handle(e);}}}privatevoidhandle(Exceptione){if(!(einstanceofSocketException)){e.printStackTrace();}}publicvoidstopProcessing(){keepProcessing=false;closeIgnoringException(serverSocket);}voidprocess(Socketsocket){if(socket==null)return;try{System.out.printf("Server:gettingmessage\n");Stringmessage=MessageUtils.getMessage(socket);System.out.printf("Server:gotmessage:%s\n",message);Thread.sleep(1000);System.out.printf("Server:sendingreply:%s\n",message);MessageUtils.sendMessage(socket,"Processed:"+message);System.out.printf("Server:sent\n");closeIgnoringException(socket);}catch(Exceptione){e.printStackTrace();}}privatevoidcloseIgnoringException(Socketsocket){if(socket!=null)try{socket.close();}catch(IOExceptionignore){}}privatevoidcloseIgnoringException(ServerSocketserverSocket){if(serverSocket!=null)try{serverSocket.close();}catch(IOExceptionignore){}}}代碼清單A-4ClientTest.javapackagecom.objectmentor.clientserver.nonthreaded;importjava.io.IOException;import.ServerSocket;import.Socket;import.SocketException;importcommon.MessageUtils;publicclassServerimplementsRunnable{ServerSocketserverSocket;volatilebooleankeepProcessing=true;publicServer(intport,intmillisecondsTimeout)throwsIOException{serverSocket=newServerSocket(port);serverSocket.setSoTimeout(millisecondsTimeout);}publicvoidrun(){System.out.printf("ServerStarting\n");while(keepProcessing){try{System.out.printf("acceptingclient\n");Socketsocket=serverSocket.accept();System.out.printf("gotclient\n");process(socket);}catch(Exceptione){handle(e);}}}privatevoidhandle(Exceptione){if(!(einstanceofSocketException)){e.printStackTrace();}}publicvoidstopProcessing(){keepProcessing=false;closeIgnoringException(serverSocket);}voidprocess(Socketsocket){if(socket==null)return;try{System.out.printf("Server:gettingmessage\n");Stringmessage=MessageUtils.getMessage(socket);System.out.printf("Server:gotmessage:%s\n",message);Thread.sleep(1000);System.out.printf("Server:sendingreply:%s\n",message);MessageUtils.sendMessage(socket,"Processed:"+message);System.out.printf("Server:sent\n");closeIgnoringException(socket);}catch(Exceptione){e.printStackTrace();}}privatevoidcloseIgnoringException(Socketsocket){if(socket!=null)try{socket.close();}catch(IOExceptionignore){}}privatevoidcloseIgnoringException(ServerSocketserverSocket){if(serverSocket!=null)try{serverSocket.close();}catch(IOExceptionignore){}}}代碼清單A-5MessageUtils.javapackagecommon;importjava.io.IOException;importjava.io.InputStream;importjava.io.ObjectInputStream;importjava.io.ObjectOutputStream;importjava.io.OutputStream;import.Socket;publicclassMessageUtils{publicstaticvoidsendMessage(Socketsocket,Stringmessage)throwsIOException{OutputStreamstream=socket.getOutputStream();ObjectOutputStreamoos=newObjectOutputStream(stream);oos.writeUTF(message);oos.flush();}publicstaticStringgetMessage(Socketsocket)throwsIOException{InputStreamstream=socket.getInputStream();ObjectInputStreamois=newObjectInputStream(stream);returnois.readUTF();}}\hA.10.2使用線程的客戶端/服務(wù)器代碼把服務(wù)器修改為使用多線程,只需要對處理消息進行修改即可(新的代碼行用粗體標出):voidprocess(finalSocketsocket){if(socket==null)return;RunnableclientHandler=newRunnable(){publicvoidrun(){try

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負責。
  • 6. 下載文件中如有侵權(quán)或不適當內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論