編程思想 -- 第21章 -- 并發(fā)

并發(fā)

并行編程可以讓程序執(zhí)行速度得到極大提高,或者為設(shè)計(jì)某些類(lèi)型的程序提供更易用的模型,或者兩者皆有。了解并發(fā)可以使你意識(shí)到明顯正確的程序可能會(huì)展示出不正確的行為。理解并發(fā)編程,其難度與理解面向?qū)ο缶幊滩畈欢唷?/p>

一、并發(fā)的多面性

用并發(fā)解決的問(wèn)題大體上可以分為速度和設(shè)計(jì)可管理型兩種。

1,更快的執(zhí)行:

并發(fā)是用于多處理器編程的基本工具,并發(fā)通常是提高運(yùn)行在單處理器上的程序的性能。事實(shí)上,從性能角度看,如果沒(méi)有任務(wù)會(huì)阻塞,那么在單處理器上使用并發(fā)就沒(méi)有任何意義。

在單處理器系統(tǒng)中的性能提高的常見(jiàn)示例是事件驅(qū)動(dòng)的編程。實(shí)現(xiàn)并發(fā)最直接的方式就是在操作系統(tǒng)級(jí)別使用進(jìn)程。進(jìn)程是運(yùn)行在它自己的地址空間內(nèi)的自包容的程序。多任務(wù)操作系統(tǒng)可以通過(guò)周期性地將CPU從一個(gè)進(jìn)程切換到另一個(gè)進(jìn)程,來(lái)實(shí)現(xiàn)同時(shí)運(yùn)行多個(gè)進(jìn)程,盡管這使得每個(gè)進(jìn)程看起來(lái)在執(zhí)行過(guò)程中都是歇歇停停。

在多任務(wù)操作系統(tǒng),可以將每個(gè)復(fù)制操作當(dāng)作單獨(dú)的進(jìn)程來(lái)啟動(dòng),并讓它們并行地運(yùn)行,這樣可以加速整個(gè)程序的執(zhí)行速度,當(dāng)一個(gè)進(jìn)程受阻時(shí),另一個(gè)進(jìn)程可以繼續(xù)前進(jìn)。這是并發(fā)的理想示例

每個(gè)任務(wù)都作為進(jìn)程在其自己的地址空間中執(zhí)行,因此任務(wù)之間根本不可能相互干涉,更重要的是,對(duì)進(jìn)程來(lái)說(shuō),它們之間沒(méi)有任何彼此通信的需要,因?yàn)樗鼈兌际峭耆?dú)立的。操作系統(tǒng)會(huì)處理確保文件正確復(fù)制的所有細(xì)節(jié),因此,不會(huì)有任何風(fēng)險(xiǎn),你可以獲得更快的程序,并且完全免費(fèi)。

某些編程語(yǔ)言被設(shè)計(jì)為可以將并發(fā)任務(wù)彼此隔離,這些語(yǔ)言通常被稱(chēng)為函數(shù)型語(yǔ)言,其中每個(gè)函數(shù)調(diào)用都不會(huì)產(chǎn)生任何副作用,并因此可以當(dāng)做獨(dú)立的任務(wù)來(lái)驅(qū)動(dòng)。如果程序必須大量使用并發(fā),可以考慮ErLang這類(lèi)專(zhuān)門(mén)的并發(fā)語(yǔ)言來(lái)創(chuàng)建這個(gè)部分。

2,改進(jìn)代碼設(shè)計(jì)

并發(fā)聽(tīng)過(guò)一個(gè)重要的結(jié)構(gòu)上的好處:你的程序設(shè)計(jì)可以極大簡(jiǎn)化。某些類(lèi)型的問(wèn)題,沒(méi)有并發(fā)的支持很難解決,如仿真。java的線程機(jī)制是搶占式的,這表示調(diào)度機(jī)制會(huì)周期性地中斷線程,將上下文切換到另一線程,從而為每個(gè)線程都提供時(shí)間片,使得每個(gè)線程都會(huì)分配到數(shù)量合理的時(shí)間去驅(qū)動(dòng)它的任務(wù)。

協(xié)作式系統(tǒng)的優(yōu)勢(shì)是雙重的:上下文切換的開(kāi)銷(xiāo)通常比搶占式系統(tǒng)要低廉,并且可以同時(shí)執(zhí)行的線程數(shù)量在理論上沒(méi)有任何限制。

并發(fā)需要代價(jià),包含復(fù)雜性代價(jià),但是這些代價(jià)與在程序設(shè)計(jì),資源負(fù)載均衡以及用戶(hù)方便使用方面的改進(jìn)相比,顯得微不足道。通常,線程使你能夠創(chuàng)建更加松散耦合的設(shè)計(jì),否則代碼中各個(gè)部分必須顯示地關(guān)注通常由線程來(lái)處理的任務(wù)。

二、基本的線程機(jī)制

并發(fā)編程使我們將程序劃分為多個(gè)分離的,獨(dú)立運(yùn)行的任務(wù)。通過(guò)使用多線程機(jī)制,這些獨(dú)立任務(wù)中的每一個(gè)都將由執(zhí)行線程來(lái)驅(qū)動(dòng)。線程簡(jiǎn)化了在單一程序中同時(shí)交織在一起的多個(gè)操作的處理。使用線程機(jī)制是一種建立透明的可擴(kuò)展的程序的方法。多任務(wù)和多線程往往是使用多處理器系統(tǒng)的最合理的方式。

1,定義任務(wù)。

線程可以驅(qū)動(dòng)任務(wù),因此需要一種描述任務(wù)的方式,可以由Runnable接口來(lái)提供。定義惹怒我,實(shí)現(xiàn)Runnable接口并編寫(xiě)run()方法,使得該任務(wù)可以執(zhí)行你的命令。當(dāng)從Runnable導(dǎo)出一個(gè)類(lèi)時(shí),它必須具有Run()方法。要實(shí)現(xiàn)線程行為,你必須顯示地將一個(gè)任務(wù)附著到線程上。

2,Thread類(lèi)

將Runnable對(duì)象轉(zhuǎn)變?yōu)楣ぷ魅蝿?wù)的傳統(tǒng)方式是把它提交給一個(gè)Thread構(gòu)造器。Thread構(gòu)造器只需要一個(gè)Runnable對(duì)象,調(diào)用Thread對(duì)象的start()方法為該線程執(zhí)行必需的初始化操作,然后調(diào)用Runnable的run()方法,以便在新線程中啟動(dòng)該任務(wù)。一個(gè)線程會(huì)創(chuàng)建一個(gè)單獨(dú)的執(zhí)行線程,在對(duì)start()調(diào)用完成后,它仍舊會(huì)存在。

使用Thread時(shí),每個(gè)Thread先注冊(cè)了自己,確保有一個(gè)對(duì)它的引用,而且在它的任務(wù)退出其run()并死亡之前,垃圾回收器無(wú)法清楚它。

3,使用Executor

java.util.concurrent包中的執(zhí)行器(Executor)將為你管理Thread對(duì)象,從而簡(jiǎn)化了并發(fā)編程。Executor允許你管理異步任務(wù)的執(zhí)行,而無(wú)須顯式得管理線程的生命周期,Executor是啟動(dòng)任務(wù)的優(yōu)選方法。與命令模式一樣,它暴露了要執(zhí)行的單一方法。單個(gè)的Executor被用來(lái)創(chuàng)建和管理系統(tǒng)中所有的任務(wù)。

ExecutorService是使用靜態(tài)的Executor方法創(chuàng)建的,shutdown()方法防止新任務(wù)交給這個(gè)Executor,當(dāng)前線程將繼續(xù)運(yùn)行在shutdown()被調(diào)用之前提交的所有任務(wù)。

CachedThreadPool()將為每個(gè)任務(wù)都創(chuàng)建一個(gè)線程,創(chuàng)建于所需數(shù)量相同的線程,然后它回收舊線程時(shí)停止創(chuàng)建新線程。FixedThreadPool()使用了有限的線程集來(lái)執(zhí)行所提交的任務(wù),可以一次性預(yù)先執(zhí)行代價(jià)高昂的線程分配,可以限制線程的數(shù)量。SingleThreadExecutor()是線程數(shù)量為1的FixedThreadPool(),提交多個(gè)任務(wù)時(shí),將任務(wù)排隊(duì),每個(gè)任務(wù)結(jié)束前運(yùn)行,所有任務(wù)使用相同線程,它序列化所有提交的任務(wù),并維護(hù)它的懸掛任務(wù)隊(duì)列。

4,從任務(wù)中產(chǎn)生返回值

Runnable是執(zhí)行工作的獨(dú)立任務(wù),但是它不返回任何值。如果希望返回一個(gè)值,可以實(shí)現(xiàn)Callable接口而不是Runnable接口。Callable是一種具有類(lèi)型參數(shù)的泛型,它的類(lèi)型參數(shù)表示從方法call()中返回的值,并且必須使用ExecutorService.submit()方法調(diào)用它。submit()方法產(chǎn)生Future對(duì)象,它用Callable返回結(jié)果的特定類(lèi)型進(jìn)行參數(shù)化,用isDone()方法查詢(xún)Future是否已經(jīng)完成。任務(wù)完成,調(diào)用get()來(lái)獲取該結(jié)果。

5,休眠

影響任務(wù)行為的一種簡(jiǎn)單方法是調(diào)用sleep(),這將使任務(wù)中止執(zhí)行給定的時(shí)間。對(duì)sleep()調(diào)用可以?huà)伋鯥nterruptedException異常,異常不能跨線程傳播。所以你必須在本地處理所有在任務(wù)內(nèi)部產(chǎn)生的異常。順序行為依賴(lài)于底層的線程機(jī)制,這種機(jī)制在不同的操作系統(tǒng)之間是有差異的,因此你不能依賴(lài)它。

6,優(yōu)先級(jí)

線程的優(yōu)先級(jí)將該線程的重要性傳遞給了調(diào)度器。盡管CPU處理現(xiàn)有線程集的順序是不確定的,但是調(diào)度器將傾向于讓優(yōu)先權(quán)最高的線程先執(zhí)行,優(yōu)先級(jí)較低的線程僅僅是執(zhí)行的頻率較低。可以使用getPriority()來(lái)讀取現(xiàn)有線程的優(yōu)先級(jí),并自任何時(shí)刻都通過(guò)setPriority()來(lái)修改它。

7,讓步

調(diào)用yield()時(shí),是建議具有相同優(yōu)先級(jí)的其他線程可以運(yùn)行。代表可以讓別的線程使用CPU,不代表被采納執(zhí)行。

8,后臺(tái)線程

所謂后臺(tái)線程,是指在程序運(yùn)行的時(shí)候在后臺(tái)提供一種通用服務(wù)的線程,并且這種線程不屬于程序中不可或缺的部分。當(dāng)所有的非后臺(tái)線程結(jié)束時(shí),程序就終止了,同事會(huì)殺死進(jìn)程中的所有后臺(tái)線程,只要有任何非后臺(tái)線程還在運(yùn)行,程序就不會(huì)終止。

9,編碼的變體

任務(wù)類(lèi)除了實(shí)現(xiàn)Runnable接口外,在非常簡(jiǎn)單的情況下,你可以直接從Thread繼承這種可替換的方式。

10,術(shù)語(yǔ)

Thread類(lèi)自身不執(zhí)行任何操作,它只是驅(qū)動(dòng)賦予它的任務(wù),Runnable接口類(lèi)執(zhí)行能做的事情。

11,加入一個(gè)線程

一個(gè)線程可以在其它線程上調(diào)用join()方法,效果是等待一段時(shí)間直到第二個(gè)線程結(jié)束才繼續(xù)執(zhí)行。join()方法可以被中斷,在線程調(diào)用中調(diào)用interrupt()方法。要實(shí)現(xiàn)線程行為,你必須顯示地將一個(gè)任務(wù)附著到線程上。

12,創(chuàng)建有響應(yīng)的用戶(hù)界面

使用線程的動(dòng)機(jī)就是建立一個(gè)有響應(yīng)的用戶(hù)界面。

13線程組

線程組持有一個(gè)線程集合。(最好把線程組看成一次不成功的嘗試,你只要忽略它就好了)

14,捕獲異常

由于線程的本質(zhì)特征,使得你不能捕獲從線程中逃逸的異常。Thread.UncaughtExceptionHandler接口,允許你在每個(gè)Thread對(duì)象對(duì)附著一個(gè)異常處理器。

三、共享受限資源

可以把單線程程序當(dāng)作在問(wèn)題域求解的單一實(shí)體,每次只能做一件事情。有了并發(fā)可以同時(shí)做多件事情,但是兩個(gè)或多個(gè)線程彼此相互干涉的問(wèn)題就出現(xiàn)了。

1,不正確的訪問(wèn)資源

共享公共資源,消除所謂競(jìng)爭(zhēng)條件,即兩個(gè)或更多的任務(wù)競(jìng)爭(zhēng)響應(yīng)某個(gè)條件,因此產(chǎn)生沖突或不一致結(jié)果的情況。

2,解決共享資源競(jìng)爭(zhēng)

使用線程的一個(gè)基本問(wèn)題:你永遠(yuǎn)不知道一個(gè)線程何時(shí)在運(yùn)行。線程被掛起是你在編寫(xiě)并發(fā)編程時(shí)需要處理的問(wèn)題。對(duì)于并發(fā)工作,你需要某種方式防止兩個(gè)任務(wù)訪問(wèn)相同資源,至少關(guān)鍵階段不能出現(xiàn)這種狀況。

防止這種沖突的辦法是當(dāng)資源被一個(gè)任務(wù)使用時(shí),為其加鎖。解鎖時(shí),另一個(gè)任務(wù)可以鎖定并使用它?;旧纤械牟l(fā)模式在解決線程沖突問(wèn)題的時(shí)候,都是采用序列化訪問(wèn)共享資源的方案,這意味著在給定的時(shí)刻只允許一個(gè)任務(wù)訪問(wèn)資源,通常是在代碼前加一條鎖語(yǔ)句實(shí)現(xiàn),使得一端時(shí)間內(nèi)只有一個(gè)任務(wù)可以運(yùn)行這段代碼,鎖語(yǔ)句實(shí)現(xiàn)了互相排斥的效果,這種機(jī)制常常稱(chēng)為互斥量。

java提供synchronized的形式,為防止資源沖突提供內(nèi)置支持。當(dāng)任務(wù)要被synchronied關(guān)鍵字保護(hù)的代碼片段時(shí),它將檢查代碼是否可用,然后獲取鎖,執(zhí)行代碼,釋放鎖。在當(dāng)前線程從該方法返回之前,其他所有調(diào)用類(lèi)中任何標(biāo)記為synchronized方法的線程都會(huì)被阻塞。

注意:在使用并發(fā)時(shí),將域設(shè)置為private是非常重要的,否則,synchronized關(guān)鍵字就不能防止其它任務(wù)直接訪問(wèn)域,這樣就會(huì)產(chǎn)生沖突。JVM負(fù)責(zé)跟蹤對(duì)象被加鎖的次數(shù),對(duì)象被解鎖,計(jì)數(shù)變?yōu)?,對(duì)象加鎖,計(jì)數(shù)變?yōu)?,每當(dāng)相同的任務(wù)在這個(gè)對(duì)象上獲得鎖時(shí),計(jì)數(shù)都會(huì)遞增,當(dāng)任務(wù)離開(kāi)synchronized方法,計(jì)數(shù)遞減,計(jì)數(shù)為零的時(shí)候,鎖被完全釋放,別的任務(wù)就可以使用此資源。

如果在你的類(lèi)中有超過(guò)一個(gè)方法處理臨街?jǐn)?shù)據(jù),你必須同步所有相關(guān)的方法。每個(gè)訪問(wèn)臨界共享資源的方法都必須被同步,否則它們就不會(huì)正確地工作。

java.util.concurrent.locks中的顯式互斥機(jī)制。Lock對(duì)象必須被顯式地創(chuàng)建,鎖定和釋放,它與內(nèi)建的鎖形式相比,代碼缺乏優(yōu)雅性,但對(duì)于某些類(lèi)型的問(wèn)題來(lái)說(shuō),它更加靈活。

大體上,當(dāng)你使用synchronized關(guān)鍵字時(shí),需要寫(xiě)的代碼量更少,并且用戶(hù)錯(cuò)誤出現(xiàn)的可能性也會(huì)降低,因此通常只有在解決特殊問(wèn)題時(shí),才使用顯式的Lock對(duì)象。顯式的Lock對(duì)象在加鎖和解鎖方面,相對(duì)于synchronized鎖來(lái)說(shuō),還賦予更細(xì)粒度的控制力,這對(duì)于實(shí)現(xiàn)專(zhuān)有同步結(jié)構(gòu)是很有用的。

3,原子性與易變性

有關(guān)java線程的討論中,一個(gè)常不正確的知識(shí)是:原子操作不需要進(jìn)行同步控制。原子操作是不能被線程調(diào)度機(jī)制中斷的操作。一旦操作開(kāi)始,那么它一定可以在可能發(fā)生的上下文切換之前執(zhí)行完畢。原子操作可由線程機(jī)制來(lái)保證其不可中斷。應(yīng)用中不具備用原子替換同步的能力。

相對(duì)于單處理器系統(tǒng)而言,可視性問(wèn)題遠(yuǎn)比原子性問(wèn)題多得多。volatile關(guān)鍵字還確保了應(yīng)用中的可視性,如果將一個(gè)域聲明為volatile的,那么只要對(duì)這個(gè)域產(chǎn)生了寫(xiě)操作,那么所有的讀操作都可以看到這個(gè)修改。如果一個(gè)域完全由synchronized方法或語(yǔ)句塊來(lái)防護(hù),那就不必將其設(shè)置為是volatile的。

使用volatile而不是synchronized的唯一安全情況是類(lèi)中只有一個(gè)可變的域。如果你將域定義為volatile,它會(huì)告訴編譯器不要執(zhí)行任何移除讀取和寫(xiě)入操作的優(yōu)化,這些操作的目的是用線程中的局部變量維護(hù)對(duì)這個(gè)域的精準(zhǔn)同步,實(shí)際上,讀取和寫(xiě)入都是直接針對(duì)內(nèi)存的,而卻沒(méi)有緩存,但是volatile并不能對(duì)遞增不是原子性操作這一事實(shí)產(chǎn)生影響。

4,原子類(lèi)

Java中如AtomicInteger、AtomicLong、AtomicReference等特殊的原子性變量類(lèi),在使用它們時(shí),通常不需要擔(dān)心,涉及到性能調(diào)優(yōu)時(shí),它們就大有用武之地。Athomic類(lèi)被設(shè)計(jì)用來(lái)構(gòu)建java.util.concurrent中的類(lèi),通常頭特殊情況下才使用它們,使用了也要確保不存在可能出現(xiàn)的問(wèn)題,通常依賴(lài)于鎖更安全些。

5,臨界區(qū)

有時(shí),你只是希望防止多個(gè)線程同時(shí)訪問(wèn)方法內(nèi)部的部分代碼而不是防止訪問(wèn)整個(gè)方法。通過(guò)這種方式分離出來(lái)的代碼段被稱(chēng)為臨界區(qū),它也是使用synchronized關(guān)鍵字建立。這也被稱(chēng)為同步代碼塊??梢允苟鄠€(gè)任務(wù)訪問(wèn)對(duì)象的時(shí)間性能得到顯著提高。

6,在其他對(duì)象上同步

synchronized塊必須給定一個(gè)在其上進(jìn)行同步的對(duì)象,并且最合理的方式是,使用其方法正在被調(diào)用的當(dāng)前對(duì)象:synchronized(this)。如果在this上同步,臨界區(qū)的效果就會(huì)直接縮小在同步的范圍內(nèi)。

7,線程本地存儲(chǔ)

防止任務(wù)在共享資源上產(chǎn)生沖突的第二種方式是根除對(duì)變量的共享。線程本地存儲(chǔ)是一種自動(dòng)化機(jī)制,可以為使用相同變量的每個(gè)不同的線程都創(chuàng)建不同的存儲(chǔ)。ThreadLoca對(duì)象通常當(dāng)做靜態(tài)域存儲(chǔ)。

四、終結(jié)任務(wù)

某些情況下,任務(wù)必須突然終止。

1,裝飾性花園

通過(guò)控制同步代碼塊內(nèi)部計(jì)數(shù)的方式來(lái)統(tǒng)計(jì)線程數(shù)量,并以此進(jìn)行中斷,讓步。

2,在阻塞時(shí)終結(jié)

sleep()使任務(wù)從執(zhí)行狀態(tài)變?yōu)楸蛔枞麪顟B(tài),而有時(shí)你必須中止被阻塞的任務(wù),或者強(qiáng)制任務(wù)跳出阻塞狀態(tài)。

線程狀態(tài):新建(new)-->就緒(Runnable)-->阻塞(Blocked)-->死亡(Dead)

進(jìn)入阻塞狀態(tài):原因如下: 1,調(diào)用sleep()使任務(wù)進(jìn)入休眠。 2,通過(guò)wait()使線程掛起。 3,任務(wù)在等待某個(gè)輸入輸出完成。??? 4,任務(wù)試圖調(diào)用同步控制方法,但另一個(gè)任務(wù)獲取此鎖。

stop()方法已被廢除,因?yàn)樗会尫啪€程獲得的鎖。suspend()和resume()用來(lái)阻塞和喚醒線程,但是已被廢止,因?yàn)榭赡軐?dǎo)致死鎖。

3,中斷

當(dāng)你打斷被阻塞的資源時(shí),可能需要清理資源,在任務(wù)的run()方法中間打斷,更像是拋出的異常,因此在java線程中的這種類(lèi)型的異常中斷中用到了異常。Thread類(lèi)包含interrupt()方法,因此可以終止被阻塞的任務(wù),將方法設(shè)置線程的中斷狀態(tài),可以打斷被互斥所阻塞的調(diào)用。

4,檢查中斷

當(dāng)你在線程上調(diào)用interrupt()時(shí),中斷發(fā)生的唯一時(shí)刻是在任務(wù)要進(jìn)入到阻塞操作中,或者已經(jīng)在阻塞操作內(nèi)部時(shí)(除了不餓中斷的I/O或被阻塞的synchronized方法之外)。

被設(shè)計(jì)用來(lái)響應(yīng)interrupt()的類(lèi)必須建立一種策略,來(lái)確保它將保持一致的狀態(tài)。這通常意味著所有需要?jiǎng)?chuàng)建的對(duì)象操作的后面,都必須緊跟try-finally字句,從而使得無(wú)論run()循環(huán)如何退出,清理都會(huì)發(fā)生。

五、線程之間的協(xié)作

當(dāng)你使用線程來(lái)同時(shí)運(yùn)行多個(gè)任務(wù)時(shí),可以通過(guò)使用鎖來(lái)同步兩個(gè)任務(wù)的行為,從而使得一個(gè)任務(wù)不會(huì)干涉另一個(gè)任務(wù)的資源。當(dāng)任務(wù)寫(xiě)作時(shí),關(guān)鍵問(wèn)題是這些任務(wù)之間的握手。為了實(shí)現(xiàn)這種握手,我們使用了相同的基礎(chǔ)特性:互斥?;コ饽軌虼_保只有一個(gè)任務(wù)可以響應(yīng)某個(gè)信號(hào),這樣就可以根除任何可能的競(jìng)爭(zhēng)條件。

1,wait()與notifyAll()

wait()使你可以等待某個(gè)條件發(fā)生變化,而改變這個(gè)條件超出了當(dāng)前方法的控制能力。wait()會(huì)在外部世界產(chǎn)生變化的時(shí)候?qū)⑷蝿?wù)掛起,并且只有在notify()或notifyAll()發(fā)生時(shí),表示發(fā)生了某些感興趣的事物,這個(gè)任務(wù)才會(huì)被喚醒并去檢查所產(chǎn)生的變化,因此wait()提供了一種在任務(wù)之間對(duì)活動(dòng)同步的方式。

調(diào)用sleep()的時(shí)候,鎖并沒(méi)有被釋放,調(diào)用yield()也屬于這種情況。當(dāng)任務(wù)在方法里對(duì)wait()的調(diào)用,線程的執(zhí)行被掛起,對(duì)象上的鎖被釋放,另外一個(gè)任務(wù)可以獲得這個(gè)鎖,因此在改對(duì)象中的其他synchronized方法可以在wait()期間被調(diào)用。

與sleep()不同,對(duì)wait()而言,1,在wait()期間對(duì)象所都是釋放的。2,可以通過(guò)notify(),notifyAll()或者令時(shí)間到期,從wait()中恢復(fù)執(zhí)行。

wait(),notify()和notifyAll()有一個(gè)比較特殊的方面,這些方法是基類(lèi)Object的一部分,而不是屬于Thread的一部分。實(shí)際上,只能在同步代碼塊里調(diào)用wait(),notify()和notifyAll()。消息的意思是:調(diào)用wait(),notify()和notifyAll()的任務(wù)在調(diào)用這些方法前必須擁有對(duì)象的鎖。可以讓另一個(gè)對(duì)象執(zhí)行某種操作以維護(hù)自己的鎖,要做么做的話(huà),必須首先得到對(duì)象的鎖。

當(dāng)連個(gè)線程使用notify()/wait()或notifyAll()/wait()進(jìn)行協(xié)作時(shí),有可能或錯(cuò)過(guò)某個(gè)信號(hào)??梢栽陂_(kāi)啟線程的時(shí)候通過(guò)設(shè)置競(jìng)爭(zhēng)條件控制流程。

2,notify()和notifyAll()

在技術(shù)上,可能會(huì)有多個(gè)任務(wù)在單個(gè)對(duì)象上處于wait()狀態(tài),因此調(diào)用notifyAll()比notify()更安全。如果只有一個(gè)任務(wù)處于wait()的狀態(tài),可以使用notify()代替notifyAll()。

3,生產(chǎn)者與消費(fèi)者

對(duì)于一個(gè)任務(wù)而言,只有一個(gè)單一的地點(diǎn)用于存放對(duì)象,從而使得另一個(gè)任務(wù)稍后可以使用這個(gè)對(duì)象。但是在典型的生產(chǎn)者-消費(fèi)者實(shí)現(xiàn)中,應(yīng)使用先進(jìn)先出隊(duì)列來(lái)存儲(chǔ)被生產(chǎn)和消費(fèi)的對(duì)象。

使用顯示的Lock和Condition對(duì)象:使用互斥并允許任務(wù)掛起的基本類(lèi)是Condition,你可以通過(guò)Condition上調(diào)用await()來(lái)掛起一個(gè)任務(wù),當(dāng)外部條件發(fā)生變化,以為著某個(gè)任務(wù)應(yīng)該繼續(xù)執(zhí)行時(shí),你可以通過(guò)signal()來(lái)通知這個(gè)任務(wù),從而喚醒一個(gè)任務(wù),或者調(diào)用signalAll()來(lái)喚醒所有在這個(gè)Condition上被其自身掛起的任務(wù)。Lock和Condition對(duì)象只有在更加困難的對(duì)現(xiàn)場(chǎng)問(wèn)題中才是必需的。

4,生產(chǎn)者-消費(fèi)者與隊(duì)列

wait()和notifyAll()方法以一種非常低級(jí)的方式解決了任務(wù)互操作問(wèn)題,即每次互交時(shí)都握手。你可以使用同步隊(duì)列來(lái)解決任務(wù)協(xié)作問(wèn)題,同步隊(duì)列在任何時(shí)刻都只允許一個(gè)任務(wù)插入或移除元素。在java.util.concurrent.BlockingQueue接口中提供了這個(gè)隊(duì)列,這個(gè)接口有大量的標(biāo)準(zhǔn)實(shí)現(xiàn),你可以使用LingkedBlockingQueue是一個(gè)無(wú)屆隊(duì)列,ArrayBlockingQueue具有固定尺寸,你可以在被阻塞之前,向其中防止有線數(shù)量的元素。

阻塞隊(duì)列可以解決非常大量的問(wèn)題,而其方式與wait()和notifyAll()相比,則簡(jiǎn)單并可靠的多。

5,任務(wù)間使用管道進(jìn)行輸入輸出

通過(guò)輸入輸出在線程間進(jìn)行通信通常很有用。提供線程功能的類(lèi)庫(kù)以管道的形式對(duì)線程簡(jiǎn)的輸入輸入提供了支持。在java輸入輸出對(duì)應(yīng)物是PipedWriter類(lèi)和PipedReader類(lèi)。管道基本上是一個(gè)阻塞隊(duì)列,存在于BlockingQueue的版本中。

六、死鎖

一個(gè)任務(wù)之間相互等待的連續(xù)循環(huán),沒(méi)有哪個(gè)線程能繼續(xù),稱(chēng)為死鎖。

滿(mǎn)足四個(gè)條件發(fā)生死鎖:1,互斥條件,任務(wù)使用的資源中至少有一個(gè)是不能共享的。2,至少有一個(gè)任務(wù)它必須持有一個(gè)資源且在等待一個(gè)當(dāng)前被別的任務(wù)持有的資源。3,資源不能被任務(wù)搶占,任務(wù)必須把資源釋放當(dāng)做普通事件。4,必須有循環(huán)等待。

java對(duì)死鎖并沒(méi)有提供語(yǔ)言層面上的支持,能否通過(guò)仔細(xì)設(shè)計(jì)程序來(lái)避免死鎖,這取決于你自己。

七、新類(lèi)庫(kù)中的構(gòu)件

java.util.concurrent引入了大量設(shè)計(jì)來(lái)解決并發(fā)的問(wèn)題的新類(lèi),學(xué)習(xí)使用它們將有助于編寫(xiě)更加簡(jiǎn)單而健壯的并發(fā)程序。

1,CountDownLatch

它被用來(lái)同步一個(gè)或多個(gè)任務(wù),強(qiáng)制它們等待由其他任務(wù)執(zhí)行的一組操作完成??梢韵駽ountDownLatch的任務(wù)在產(chǎn)生這個(gè)電泳時(shí)并沒(méi)有被阻塞,只有對(duì)await()的調(diào)用會(huì)被阻塞,直至計(jì)數(shù)值到達(dá)0。Random.nextInt()是安全的。

2,CyclicBarrier

CyclicBarrier適用于:創(chuàng)建一組任務(wù),并行執(zhí)行工作,然后在進(jìn)行下一個(gè)步驟前等待,直至所有任務(wù)都完成,它使得所有并行任務(wù)都將在柵欄處列隊(duì),因此可以一致地向前移動(dòng)。CountDownLathc只觸發(fā)一次,而B(niǎo)yclicBarrier可以多次重復(fù)使用。

3,DelayQueue

這是一個(gè)無(wú)界的BlckingQueue,用于放置實(shí)現(xiàn)了Delayed接口的對(duì)象,其中的對(duì)象只能在其到期時(shí)才能從隊(duì)列中取走。這種隊(duì)列是有序的,即對(duì)頭對(duì)象的延遲到期的時(shí)間最長(zhǎng)。如果沒(méi)有任何延遲到期,那么就不會(huì)有任何頭元素,并且poll()將放回null。

4,PriorityBlockingQueue

這是一個(gè)很基礎(chǔ)的優(yōu)先級(jí)隊(duì)列,它具有可阻塞的讀取操作。在優(yōu)先級(jí)隊(duì)列中的對(duì)象是按照優(yōu)先級(jí)順序從隊(duì)列中出現(xiàn)的任務(wù)。

5,使用ScheduledExecutor的溫室控制器

可以應(yīng)用于假想溫室的控制系統(tǒng)的實(shí)例,可以控制各種設(shè)施的開(kāi)關(guān),或者是對(duì)它們進(jìn)行調(diào)節(jié)。這被看作是一種并發(fā)問(wèn)題,每個(gè)期望的溫室時(shí)間都是在一個(gè)預(yù)定時(shí)間運(yùn)行的任務(wù)。

6,Semaphore

正常的鎖在任何時(shí)刻都只允許一個(gè)任務(wù)訪問(wèn)一項(xiàng)資源,而技術(shù)信號(hào)量允許n個(gè)任務(wù)同時(shí)訪問(wèn)這個(gè)資源??梢詫⑿盘?hào)量看作是在向外分發(fā)使用資源的許可證。考慮對(duì)象池的概念,它管理著數(shù)量有限的對(duì)象,使用對(duì)象時(shí)簽出它們,使用完畢后,將它們簽回。

7,Exchanger

Exchanger是在兩個(gè)任務(wù)之間交換對(duì)象的柵欄。任務(wù)進(jìn)入柵欄時(shí),各自擁有一個(gè)對(duì)象,它們離開(kāi)時(shí),它們都擁有之前對(duì)象持有的對(duì)象。典型的應(yīng)用場(chǎng)景是:一個(gè)任務(wù)在創(chuàng)建對(duì)象,這些對(duì)象的生產(chǎn)代價(jià)很高昂,另一個(gè)任務(wù)在消費(fèi)這些對(duì)象,通過(guò)這種方式,可以有更多對(duì)象在被創(chuàng)建的同時(shí)被消費(fèi)。

八、仿真

并發(fā)最有趣也是最令人興奮的用法就是創(chuàng)建仿真,通過(guò)使用并發(fā),仿真的每個(gè)構(gòu)建都可以成為其自身的任務(wù),這使得仿真更容易編程。

1,銀行出納仿真

這個(gè)經(jīng)典的仿真可以表示任務(wù)號(hào)屬于下面這種類(lèi)型的情況:對(duì)象隨機(jī)出現(xiàn),并且要求由數(shù)量有限的服務(wù)器提供隨機(jī)數(shù)量的服務(wù)時(shí)間。通過(guò)構(gòu)建仿真可以確定理想的服務(wù)器數(shù)量。

2,飯店仿真

這個(gè)仿真添加了更多的仿真組件。

3,分發(fā)工作

烤爐假想汽車(chē)的組裝線,每個(gè)汽車(chē)都分多個(gè)階段構(gòu)建,從創(chuàng)建底盤(pán)開(kāi)始,緊跟著安裝發(fā)動(dòng)機(jī),車(chē)廂,輪子。

九、性能調(diào)優(yōu)

java.util.concurrent類(lèi)存在著數(shù)量龐大的用于性能提高的類(lèi)。

Java包括老式的synchronized關(guān)鍵字和Lock類(lèi)和Atomic類(lèi),那么比較這些不同的方式,跟過(guò)理解它們各自的價(jià)值和使用范圍,就會(huì)顯得很有意義。使用Lock通常要比synchronized要高效的多,而且synchronized的開(kāi)銷(xiāo)看起來(lái)變化范圍太大,而Lock相對(duì)比較一致。

因此從synchronized關(guān)鍵字入手,只有在性能調(diào)優(yōu)時(shí)才替換為L(zhǎng)ock對(duì)象的做法,是有實(shí)際意義的。

免鎖容器:容器是所有編程中的基礎(chǔ)工具,這其中也包括并發(fā)編程。對(duì)容器的修改可以與讀取操作同事發(fā)生,只要讀者只能看到完成修改的結(jié)果即可。

樂(lè)觀鎖:只要你主要是從免鎖容器中讀取,那么它就會(huì)比其synchronized對(duì)應(yīng)物快許多,因?yàn)楂@取和釋放鎖的開(kāi)銷(xiāo)被省掉了。

樂(lè)觀加鎖:盡管Atomic對(duì)象執(zhí)行原子性操作,但是某些Atomic類(lèi)還允許執(zhí)行所謂的樂(lè)觀加鎖。

ReadWriterLock是否能夠提高程序的性能是完全不可確定的,它取決于諸如數(shù)據(jù)被讀取的頻率與被修改的頻率相比較的結(jié)果,讀取和寫(xiě)入操作的時(shí)間,有多少線程競(jìng)爭(zhēng)以及是否在多處理機(jī)器上運(yùn)行等因素。

十、活動(dòng)對(duì)象

盡管多個(gè)任務(wù)可以并行工作,但是你必須花很大的利器去實(shí)現(xiàn)防止這些任務(wù)彼此相互干涉的技術(shù)。

有一種可替換的方式被稱(chēng)為活動(dòng)對(duì)象或行動(dòng)者。之所以稱(chēng)為是活動(dòng)的,因?yàn)槊總€(gè)對(duì)象都維護(hù)著它自己的工作器線程和消息隊(duì)列,并且所有對(duì)這種對(duì)象的請(qǐng)求都將進(jìn)入隊(duì)列排隊(duì),任何時(shí)刻都只能運(yùn)行其中一個(gè)。有了活動(dòng)對(duì)象,就可以串行化消息而不是方法,這意味著不再需要防備一個(gè)任務(wù)在其循環(huán)的中間被中斷這種問(wèn)題。

活動(dòng)對(duì)象:1,每個(gè)對(duì)象都可以有自己的工作期線程。2,每個(gè)對(duì)象都將維護(hù)對(duì)它自己的域的全部控制權(quán)。3,所有在活動(dòng)對(duì)象之間的通信都將以在這些對(duì)象之間的消息形式發(fā)生。4,活動(dòng)對(duì)象之間的所有消息都要排隊(duì)。

十一、總結(jié)

本章的目標(biāo)是想你提供使用Java線程進(jìn)行并發(fā)程序設(shè)計(jì)的基礎(chǔ)知識(shí)以使你理解:1,可以運(yùn)行多個(gè)獨(dú)立的任務(wù),2,必須考慮這些任務(wù)關(guān)閉時(shí),可能出現(xiàn)的所有問(wèn)題。3,任務(wù)可能會(huì)在共享資源上彼此干涉,互斥是用來(lái)防止這種沖突的基本工具。4,如果任務(wù)設(shè)計(jì)的不夠仔細(xì),就有可能發(fā)生死鎖。

明白什么時(shí)候應(yīng)該使用并發(fā),什么時(shí)候應(yīng)該避免使用并發(fā)是非常關(guān)鍵的。使用它的主要原因是:

1,要處理很多任務(wù),它們交織在一起,應(yīng)用并發(fā)可以更有效地使用計(jì)算機(jī)。2,要能夠更好地組織代碼。3,要更便于用戶(hù)使用。

線程的一個(gè)額外好處是它們提供了輕量級(jí)的執(zhí)行上下文切換,而不是重量級(jí)的進(jìn)程上下文切換。因?yàn)橐粋€(gè)給定進(jìn)程內(nèi)的所有線程共享相同的內(nèi)存空間,輕量級(jí)的上下文切換只是改變程序的執(zhí)行序列和局部變量。進(jìn)程切換必須改變所有內(nèi)存空間。

多線程的主要缺陷:1,等待共享資源的時(shí)候性能降低。2,需要處理線程的額外CPU花費(fèi)。3,糟糕的程序設(shè)計(jì)導(dǎo)致不必要的復(fù)雜度。4,有可能產(chǎn)生一些病態(tài)行為,如餓死,競(jìng)爭(zhēng),死鎖和活鎖。5,不同平臺(tái)導(dǎo)致的不一致性。

因?yàn)槎嗑€程可能共享一個(gè)資源,比如一個(gè)對(duì)象的內(nèi)存,而且你必須確定多線程不會(huì)同時(shí)讀取和改變這個(gè)資源,這是線程產(chǎn)生的最大難題。這需要明智使用可用的加鎖機(jī)制,它們會(huì)引入潛在的死鎖條件,要對(duì)它們有透徹的理解。

線程的應(yīng)用上也有一些技巧,java允許你建立足夠都的對(duì)象來(lái)解決你的問(wèn)題。然而,你創(chuàng)建的線程數(shù)目要有個(gè)上界,因?yàn)檫_(dá)到了一定的數(shù)量,線程性能會(huì)很差。這個(gè)臨界點(diǎn)很難監(jiān)測(cè),通常依賴(lài)于操作系統(tǒng)和JVM;不過(guò)我們通常創(chuàng)建少數(shù)線程來(lái)解決問(wèn)題,所以這個(gè)限制并不嚴(yán)重。

通常使用線程機(jī)制需要非常仔細(xì)和保守。如果你的線程變得大而復(fù)雜,那么就應(yīng)該考慮使用ErLang這樣的語(yǔ)言,這是專(zhuān)門(mén)用于線程機(jī)制的幾種函數(shù)型語(yǔ)言之一。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容