首頁(yè)技術(shù)文章正文

Java編程思想

更新時(shí)間:2018-09-11 來源:黑馬程序員JavaEE培訓(xùn)學(xué)院? 瀏覽量:

Java編程思想

 

順序編程,即程序中的所有事物在任意時(shí)刻都只能執(zhí)行一個(gè)步驟。并發(fā)編程,程序能夠并行地執(zhí)行程序中的多個(gè)部分。

1 定義任務(wù)

線程可以驅(qū)動(dòng)任務(wù),因此你需要一種描述任務(wù)的方式,這可以由Runnable接口來提供。要想定義任務(wù),只需實(shí)現(xiàn)Runnable接口并編寫run()方法,使得該任務(wù)可以執(zhí)行你的命令。 
當(dāng)從Runnable導(dǎo)出一個(gè)類時(shí),它必須具有run()方法,但是這個(gè)方法并無(wú)特殊之處——它不會(huì)產(chǎn)生任何內(nèi)在的線程能力。要實(shí)現(xiàn)線程行為,你必須顯式地將一個(gè)任務(wù)附著到線程上。

2 使用Executor

FixedThreadPool 與 CachedThreadPool

FixedThreadPool, 可以一次性預(yù)先執(zhí)行代價(jià)高昂的線程分配,因而也就可以限制線程的數(shù)量了。這可以節(jié)省時(shí)間,因?yàn)槟悴挥脼槊總€(gè)任務(wù)都固定地付出創(chuàng)建線程的開銷。在事件驅(qū)動(dòng)的系統(tǒng)中,需要線程的事件處理器,通過直接從池中獲取線程,也可以如你所愿地得到服務(wù)。你不會(huì)濫用可獲得的資源,因?yàn)镕ixedThreadPool使用的Thread對(duì)象的數(shù)量是有界的。

注意,在任何線程池中,現(xiàn)有線程在可能的情況下,都會(huì)被自動(dòng)復(fù)用。

盡管本書將使用CachedThreadPool,但是也應(yīng)該考慮在產(chǎn)生線程的代碼中使用FiexedThreadPool。CachedThreadPool在程序執(zhí)行過程中通常會(huì)創(chuàng)建與所需數(shù)量相同的線程,然后在它回收舊線程時(shí)停止創(chuàng)建新線程,因此它是合理的Executor的首選。只有當(dāng)這種方式會(huì)引發(fā)問題時(shí),你才需要切換到FixedThreadPool。

SingleThreadExecutor就像是線程數(shù)量為1的FixedThreadPool。(它還提供了一種重要的并發(fā)保證,其他線程不會(huì)(即沒有兩個(gè)線程會(huì))被調(diào)用。這會(huì)改變?nèi)蝿?wù)的加鎖需求) 
如果向SingleThreadExecutor提交了多個(gè)任務(wù),那么這些任務(wù)將排隊(duì),每個(gè)任務(wù)都會(huì)在下一個(gè)任務(wù)開始之前運(yùn)行結(jié)束,所有的任務(wù)將使用相同的線程。在下面的示例中,你可以看到每個(gè)任務(wù)都是按照它們被提交的順序,并且是在下一個(gè)任務(wù)開始之前完成的。因此,SingleThreadExecutor會(huì)序列化所有提交給它的任務(wù),并會(huì)維護(hù)它自己(隱藏)的懸掛任務(wù)隊(duì)列。

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

Runnable是執(zhí)行工作的獨(dú)立任務(wù),但是它不返回任務(wù)值。如果你希望任務(wù)在完成時(shí)能夠返回一個(gè)值,那么可以實(shí)現(xiàn)Callable接口而不是Runnable接口。在Java SE5中引入的Callable是一種具有類型參數(shù)的泛型,它的類型參數(shù)表示的是從方法call()(而不是run())中返回的值,并且必須使用ExecutorService.submit()方法調(diào)用它。

4 編碼的變體

另一種可能會(huì)看到的慣用法是自管理的Runnable。

這與從Thread繼承并沒有什么特別的差異,只是語(yǔ)法稍微晦澀一些。但是,實(shí)現(xiàn)接口使得你可以繼承另一個(gè)不同的類,而從Thread繼承將不行。

注意,自管理的Runnable是在構(gòu)造器中調(diào)用的。這個(gè)示例相當(dāng)簡(jiǎn)單,因此可能是安全的,但是你應(yīng)該意識(shí)到,在構(gòu)造器中啟動(dòng)線程可能會(huì)變得很有問題,因?yàn)榱硪粋€(gè)任務(wù)可能會(huì)在構(gòu)造器結(jié)束之前開始執(zhí)行,這意味著該任務(wù)能夠訪問處于不穩(wěn)定狀態(tài)的對(duì)象。這是優(yōu)選Executor而不是顯式地創(chuàng)建Thread對(duì)象的另一個(gè)原因。

5 線程組

線程組持有一個(gè)線程集合。線程組的價(jià)值可以引用Joshua Bloch的話來總結(jié):“最好把線程組看成是一次不成功的嘗試,你只要忽略它就好了?!?/p>

 如果你花費(fèi)了大量的時(shí)間和精力試圖發(fā)現(xiàn)線程組的價(jià)值(就像我一樣),那么你可能會(huì)驚異,為什么沒有來自Sun的關(guān)于這個(gè)主題的官方聲明,多年以來,相同的問題對(duì)于Java發(fā)生的其他變化也詢問過無(wú)數(shù)遍。諾貝爾經(jīng)濟(jì)學(xué)將得主Joseph Stiglitz的生活哲學(xué)可以用來解釋這個(gè)問題,它被稱為承諾升級(jí)理論(The Theory of Escalating Commitment):“繼續(xù)錯(cuò)誤的代價(jià)由別人來承擔(dān),而承認(rèn)錯(cuò)誤的代價(jià)由自己承擔(dān)?!?/p>

6 捕獲異常

由于線程的本質(zhì)特性,使得你不能捕獲從線程中逃逸的異常。一旦異常逃出任務(wù)的run()方法,它就會(huì)向外傳播到控制臺(tái),除非你采取特殊的步驟捕獲這種錯(cuò)誤的異常。

7 共享受限資源

可以把單線程程序當(dāng)作在問題域求解的單一實(shí)體,每次只能做一件事情。

8 不正確地訪問資源

因?yàn)閏anceled標(biāo)志是boolean類型的,所以它是原子性的,即諸如賦值和返回值這樣的簡(jiǎn)單操作在發(fā)生時(shí)沒有中斷的可能,因此你不會(huì)看到這個(gè)域處于在執(zhí)行這些簡(jiǎn)單操作的過程中的中間狀態(tài)。

有一點(diǎn)很重要,那就是要注意到遞增程序自身也需要多個(gè)步驟,并且在遞增過程中任務(wù)可能會(huì)被純種機(jī)制掛起——也就是說,在Java中,遞增不是原子性的操作。因此,如果不保護(hù)任務(wù),即使單一的遞增也不是安全的。

9 終結(jié)任務(wù)
中斷

Executor上調(diào)用shutdownNow(),它將發(fā)送一個(gè)interrupt()調(diào)用給它啟動(dòng)的所有線程。

Executor 通過調(diào)用submit()而不是excutor()來啟動(dòng)任務(wù),就可以持有該任務(wù)的上下文。submit()將返回一個(gè)泛型的Future<?>,持有這種Future的關(guān)鍵在于你可以在其上調(diào)用cancel(),并因此可以使用它來中斷某個(gè)特定任務(wù)。如果你將true傳遞給cancel(),那么它就會(huì)擁有在該線程上調(diào)用interrupt()以停止這個(gè)線程的權(quán)限。因此,cancel()是一個(gè)種中斷由Excutor啟動(dòng)的單個(gè)線程的方式。

SleepBlock()是可中斷的阻塞,而IOBlocked和SynchronizedBlocked是不可中斷的阻塞。上面三個(gè)類的示例證明I/O和在synchronized塊上的等待是不可中斷的。無(wú)論是I/O還是嘗試調(diào)用synchronized方法,都不需要任何InterruptedException處理器。 
從關(guān)于上面三個(gè)類的示例的輸出中可以看到,你能夠中斷對(duì)sleep()的調(diào)用(或者任何要求拋出InterruptedException的調(diào)用)。但是,你不能中斷試圖獲取synchronized鎖或者試圖執(zhí)行I/O操作的線程。這有點(diǎn)令人煩惱,特別是在妊I/O的任務(wù)時(shí),因?yàn)檫@意味著IO具有鎖住你的多線程程序的潛在可能。特別是對(duì)于基于Web的程序,這更是關(guān)乎利害。

對(duì)于這類問題,有一個(gè)略顯笨拙但是有時(shí)確實(shí)行之有效的解決方案,即關(guān)閉任務(wù)在其上發(fā)生阻塞的底層資源:

10 線程之間的協(xié)作
wait()與notifyAll()

wait()使你可以等待某個(gè)條件發(fā)生變化,而改變這個(gè)條件超出了當(dāng)前方法的控制能力。通常,這種條件將由另一個(gè)任務(wù)來改變。你肯定不想在你的任務(wù)測(cè)試這個(gè)條件的同時(shí),不斷地進(jìn)行空循環(huán),這被稱為忙等待, 通常是一種不良的周期使用方式。因此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í)候鎖并沒有被 釋放,調(diào)用yield()也屬于這種情況,理解這一點(diǎn)很重要。 
wait(), notify()以及notifyAll()有一個(gè)比較特殊的方面,那就是這些方法是基類Object的一個(gè)部分,而不是屬于Thread的一部分。

錯(cuò)失的信號(hào)。

notify() 與 notifyAll()

在有關(guān)Java的線程機(jī)制的討論中,有一個(gè)令人困惑的描述: notifyAll()將喚醒“所有下在等等的任務(wù)”。這是否意味著在程序中任何地方,任何處于wait()狀態(tài)中的任務(wù)都將被任何對(duì)notifyAll()的調(diào)用喚醒呢?有示例說明情況并非如此——事實(shí)上,當(dāng)notifyAll()因某個(gè)特定鎖而被調(diào)用時(shí),只有等待這個(gè)鎖的任務(wù)才會(huì)被喚醒。

11  死鎖

由Edsger Dijkstrar提出的哲學(xué)家就餐問題是一個(gè)經(jīng)典的死鎖例證。

要修正死鎖問題,你必須明白,當(dāng)以下四個(gè)條件同時(shí)滿足時(shí),就會(huì)發(fā)生死鎖:

互斥條件。任務(wù)使用的資源中至少有一個(gè)是不能共享的。這里,一根Chopstick一次就只能被一個(gè)Philosopher使用。

至少有一個(gè)任務(wù)它必須持有一個(gè)資源且正在等待獲取一個(gè)當(dāng)前被別的任務(wù)持有的資源。也就是說,要發(fā)生死鎖,Philosopher必須拿著一根Chopstick并且等待另一根。

資源不能被任務(wù)搶占,任務(wù)必須把資源釋放當(dāng)作普通事件。Philosopher很有禮貌,他們不會(huì)從其他Philosopher那里搶占Chopstick。

必須有循環(huán)等待,這時(shí),一個(gè)任務(wù)等待其他任務(wù)所持有的資源,后者又在等待另一個(gè)任務(wù)所持有的漿,這樣一直下去,直到有一個(gè)任務(wù)在等待第一個(gè)任務(wù)所持有的資源,使得大家都被鎖住。在DeadlockingDiningPhilosophers.java中,因?yàn)槊總€(gè)Philosopher都試圖先得到右邊的Chopstick,然后得到左邊的Chopstick,所以發(fā)徨了循環(huán)等待。
所以要防止死鎖的話,只需破壞其中一個(gè)即可。防止死鎖最容易的方法是破壞第4個(gè)條件。

12 新類庫(kù)中的構(gòu)件
 CountDownLatch

適用場(chǎng)景:它被用來同步一個(gè)或多個(gè)任務(wù),強(qiáng)制它們等待由其他任務(wù)執(zhí)行的一組操作完成。即一個(gè)或多個(gè)任務(wù)需要等待,等待到其它任務(wù),比如一個(gè)問題的初始部分,完成為止。

你可以向CountDownLatch對(duì)象設(shè)置一個(gè)初始值,任何在這個(gè)對(duì)象上調(diào)用wait()的方法都將阻塞,直到這個(gè)計(jì)數(shù)值到達(dá)0.其他因結(jié)束其工作時(shí),可以在訪對(duì)象上調(diào)用countDown()來減小這個(gè)計(jì)數(shù)值。CountDownLatch被設(shè)計(jì)為只解發(fā)一次,計(jì)數(shù)值不能被重置。如果你需要能夠重置計(jì)數(shù)值的版本,則可以使用CyclicBarrier。

調(diào)用countDown()的任務(wù)在產(chǎn)生這個(gè)調(diào)用時(shí)并沒有被阻塞,只有對(duì)await()的調(diào)用會(huì)被阻塞,直至計(jì)數(shù)值到達(dá)0。

CountDownLatch的典型用法是將一個(gè)程序分為n個(gè)互相獨(dú)立的可解決任務(wù),并創(chuàng)建值為n的CountDownLatch。當(dāng)每個(gè)任務(wù)完成時(shí),都會(huì)在這個(gè)鎖存器上調(diào)用countDown()。等待問題被解決的任務(wù)在這個(gè)鎖存器上調(diào)用await(),將它們自己掛起,直至鎖存器計(jì)數(shù)結(jié)束。

13 CyclicBarrier

適用于這樣的情況:你希望創(chuàng)建一組任務(wù),它們并行地執(zhí)行工作,然后在進(jìn)行下一下步驟之前等待,直至所有任務(wù)都完成(看起來有些像Join())。它使得所有的并行任務(wù)都將在柵欄處列隊(duì),因此可以一致地向前移動(dòng)。

例如程序賽馬程序:HorseRace.java

14 DelayQueue

DelayQueue是一個(gè)無(wú)界的BlockingQueue(同步隊(duì)列),用于放置實(shí)現(xiàn)了Delayed接口的對(duì)象,其中的對(duì)象只能在其到期時(shí)才能從隊(duì)列中取走。這種隊(duì)列是有序的,即隊(duì)頭對(duì)象是最先到期的對(duì)象。如果沒有到期的對(duì)象,那么隊(duì)列就沒有頭元素,所以poll()將返回null(也正因?yàn)榇?,我們不能將null放置到這種隊(duì)列中)。如上所述,DelayQueue就成為了優(yōu)先級(jí)隊(duì)列的一種變體。

15 PriorityBlockingQueue

這是一個(gè)很基礎(chǔ)的優(yōu)先級(jí)隊(duì)列,它具有可阻塞的讀取操作。這種隊(duì)列的阻塞特性提供了所有必需的同步,所以你應(yīng)該注意到了,這里不需要任何顯式的同步——不必考慮當(dāng)你從這種隊(duì)列中讀取時(shí),其中是否有元素,因?yàn)檫@個(gè)隊(duì)列在沒有元素時(shí),將直接阻塞讀取者。

16 使用ScheduledExecutor的室溫控制器

“溫室控制系統(tǒng)”可以被看作是一種并發(fā)問題,每個(gè)期望的溫室事件都是一個(gè)預(yù)定時(shí)間運(yùn)行的任務(wù)。 
ScheduledThreadPoolExecutor可以解決這種問題。其中schedule()用來運(yùn)行一次任務(wù),scheduleAtFixedRate()每隔規(guī)定的時(shí)間重復(fù)執(zhí)行任務(wù)。兩個(gè)方法接收delayTime參數(shù)??梢詫unnable對(duì)象設(shè)置為在將來的某個(gè)時(shí)刻執(zhí)行。

17 Semaphre

BlockingQueue: 同步隊(duì)列,當(dāng)?shù)谝粋€(gè)元素為空或不可用時(shí),執(zhí)行.take()時(shí),等待(阻塞、Blocking)。

SynchronousQueue: 是一種沒有內(nèi)部容量的阻塞隊(duì)列,因此每個(gè)put()都必須等待一個(gè)take(),反之亦然(即每個(gè)take()都必須等待一個(gè)put())。這就好像你在把一個(gè)對(duì)象交給某人——沒有任何桌子可以放置這個(gè)對(duì)象,因此只有在這個(gè)人伸出手,準(zhǔn)備好接收這個(gè)對(duì)象時(shí),你才能工作。在本例中,SynchronousQueue表示設(shè)置在用餐者面前的某個(gè)位置,以加強(qiáng)在任何時(shí)刻只能上一道菜這個(gè)概念。

關(guān)于這個(gè)示例,需要觀察的一項(xiàng)非常重要的事項(xiàng),就是使用隊(duì)列在任務(wù)間通信所帶來的管理復(fù)雜度。這個(gè)單項(xiàng)技術(shù)通過反轉(zhuǎn)控制極大地簡(jiǎn)化了并發(fā)編程的過程:任務(wù)沒有直接地互相干涉,而是經(jīng)由隊(duì)列互相發(fā)送對(duì)象。接收任務(wù)將處理對(duì)象,將其當(dāng)作一個(gè)消息來對(duì)待,而不是向它發(fā)送消息。如果只要可能就遵循這項(xiàng)技術(shù),那么你構(gòu)建出健壯的并發(fā)系統(tǒng)的可能性就會(huì)大大增加。

18 分發(fā)工作
性能調(diào)優(yōu)(Performance Tuning) 比較各類互斥技術(shù)(Comparing mutex technologies)

“微基準(zhǔn)測(cè)試(microbenchmarking)”危險(xiǎn):這個(gè)術(shù)語(yǔ)通常指在隔離的、脫離上下文環(huán)境的情況下對(duì)某個(gè)特性進(jìn)行性能測(cè)試。當(dāng)然,你仍舊必須編寫測(cè)試來驗(yàn)證諸如“Lock比synchronized更快”這樣的斷言,但是你需要在編寫這些測(cè)試的進(jìn)修意識(shí)到,在編譯過程中和在運(yùn)行時(shí)實(shí)際會(huì)發(fā)生什么。

不同的編譯器和運(yùn)行時(shí)系統(tǒng)在這方面會(huì)有所差異,因此很難確切了解將會(huì)發(fā)生什么,但是我們需要防止編譯器去預(yù)測(cè)結(jié)果的可能性。

使用Lock通常會(huì)比使用synchronized要高效許多,而且synchronized的開銷看起來變化范圍太大,而Lock相對(duì)比較一致。 
這是否意味著你永遠(yuǎn)都不應(yīng)該使用synchronized關(guān)鍵字呢?這里有兩個(gè)因素需要考慮:
一是互斥方法的方法體的大小。

二是synchronized關(guān)鍵字所產(chǎn)生的代碼與Lock所需的“加鎖-try/finally-解鎖”慣用法所產(chǎn)生的代碼相比,可讀性提高了很多。

代碼被閱讀的次數(shù)遠(yuǎn)多于被編寫的次數(shù)。在編程時(shí),與其他人交流相對(duì)于與計(jì)算機(jī)交流而言,要重要得多,因此代碼的可讀性至關(guān)重要。因此,以synchronized關(guān)鍵字入手,只有在性能調(diào)優(yōu)時(shí)才替換為L(zhǎng)ock對(duì)象這種做法,是具有實(shí)際意義的。

19 免鎖容器(Lock-free containers)

這些免鎖窗口的通用策略是:對(duì)容器的修改可以與讀取操作同時(shí)發(fā)生,只要讀取者只能看到完成修改的結(jié)果婀。修改是在容器數(shù)據(jù)結(jié)構(gòu)的某個(gè)部分的一個(gè)單獨(dú)的副本(有時(shí)是整個(gè)數(shù)據(jù)結(jié)構(gòu)的副本)上執(zhí)行的,并且這個(gè)副本在修改過程中是不可視的。只有當(dāng)修改完成時(shí),被修改的結(jié)構(gòu)都會(huì)自動(dòng)地與主數(shù)據(jù)結(jié)構(gòu)進(jìn)行交換,之后讀取者就可以看到這個(gè)修改了。

樂觀鎖

只要你主要是從免鎖容器中讀取,那么它就會(huì)比其synchronized對(duì)應(yīng)物快許多,因?yàn)楂@取和釋放鎖的開銷被省掉了。如果需要向免鎖容器中執(zhí)行少量寫入,那么情況仍舊如此,但是什么算“少量”?這是一個(gè)很有意思的問題。

20 總結(jié)

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

本文版權(quán)歸黑馬程序員JavaEE學(xué)院所有,歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)注明作者出處。謝謝!

作者:黑馬程序員JavaEE培訓(xùn)學(xué)院

首發(fā):http://java.itheima.com/

 

分享到:
在線咨詢 我要報(bào)名
和我們?cè)诰€交談!