首頁  >  文章  >  Java  >  Java程式設計思想學習課程(八)第21章-並發

Java程式設計思想學習課程(八)第21章-並發

php是最好的语言
php是最好的语言原創
2018-08-09 15:01:541317瀏覽

   順序程式設計,也就是程式中的所有事物在任何時刻都只能執行一個步驟。 並發程式設計,程式能夠並行地執行程式中的多個部分。

21.2.1 定義任務

  執行緒可以驅動任務,因此你需要一種描述任務的方式,這可以由Runnable介面來提供。要定義任務,只需實作Runnable介面並寫run()方法,使得該任務可以執行你的指令。
當從Runnable匯出一個類別時,它必須有run()方法,但這個方法並無特殊之處-它不會產生任何內在的執行緒能力。要實作執行緒行為,你必須明確地將一個任務附著到執行緒上

21.2.3 使用Executor

  FixedThreadPoolCachedThreadPool#  

  • FixedThreadPool

#, 可以一次預先執行代價高昂的執行緒分配,因而也就可以限制執行緒的數量了。這可以節省時間,因為你不用為每個任務固定地付出創建線程的開銷。在事件驅動的系統中,需要線程的事件處理器,透過直接從池中取得線程,也可以如你所願地得到服務。你不會濫用可取得的資源,因為FixedThreadPool所使用的Thread物件的數量是有界的。
  •   注意,在任何執行緒池中,現有執行緒在可能的情況下,都會被自動重複使用。 儘管本書會使用CachedThreadPool,但也應該考慮在產生執行緒的程式碼中使用FiexedThreadPoolCachedThreadPool

    在程式執行過程中通常會建立與所需數量相同的線程,然後在它回收舊線程時停止建立新線程,因此它是合理的
  • Executor
  • 的首選。只有當這種方式會引發問題時,你才需要切換到

    FixedThreadPoolSingleThreadExecutor
    就像是執行緒數為1FixedThreadPool。 (它還提供了一個重要的並發保證,其他線程不會(即沒有兩個線程會)被調用。這會改變任務的加鎖需求)

    如果向
  • SingleThreadExecutor
提交了多個任務,那麼這些任務將排隊,每個任務都會在下一個任務開始之前執行結束,所有的任務將使用相同的執行緒。在下面的範例中,你可以看到每個任務都是按照它們被提交的順序,並且是在下一個任務開始之前完成的。因此,

SingleThreadExecutor

會序列化所有提交給它的任務,並將維護它自己(隱藏)的懸掛任務佇列。

21.2.4 從任務中產生回傳值  Runnable是執行工作的獨立任務,但它不會傳回任務值。如果你希望任務在完成時能夠回傳一個值,那麼可以實作Callable介面而不是Runnable介面。在Java SE5中引入的Callable是一種具有型別參數的泛型,它的型別參數表示的是從方法call()

(而不是

run()

)中傳回的值,並且必須使用

ExecutorService.submit()方法來呼叫它。

21.2.9 編碼的變體

  另一種可能會看到的慣用法是自管理的Runnable

  這與從

Thread繼承並沒有什麼特別的差異,只是語法稍微晦澀一些。但是,實作介面使得你可以繼承另一個不同的類,而從Thread繼承將不行。   注意,自管理的Runnable

是在建構器中呼叫的。這個範例相當簡單,因此可能是安全的,但是你應該意識到,在建構器中啟動執行緒可能會變得很有問題,因為另一個任務可能會在建構器結束之前開始執行,這意味著該任務能夠存取處於不穩定狀態的物件。這是優選

Executor

而不是明確地創建
Thread對

象的另一個原因。 21.2.13 執行緒組

######  執行緒組持有一個執行緒集合。線程組的價值可以引用Joshua Bloch的話來總結:「最好把線程組看成是一次不成功的嘗試,你只要忽略它就好了。」### #############################################################################

  如果你花了大量的時間和精力試圖發現線程組的價值(就像我一樣),那麼你可能會驚異,為什麼沒有來自Sun的關於這個主題的官方聲明,多年以來,相同的問題對於Java發生的其他變化也詢問過無數遍。諾貝爾經濟學將得主Joseph Stiglitz的生活哲學可以用來解釋這個問題,它被稱為承諾升級理論(The Theory of Escalating Commitment):「繼續錯誤的代價由別人來承擔,而承認錯誤的代價由自己承擔。”

21.2.14 捕獲異常

  由於線程的本質特性,使得你不能捕獲從線程中逃逸的異常。一旦異常逃出任務的run()方法,它就會向外傳播到控制台,除非你採取特殊的步驟來捕捉這種錯誤的異常。

21.3 共享受限資源

  可以把單執行緒程式當作在問題域求解的單一實體,每次只能做一件事情。

21.3.1 不正確地存取資源

  因為canceled標誌是boolean類型的,所以它是原子性的,即諸如賦值和回傳值這樣的簡單操作在發生時沒有中斷的可能,因此你不會看到這個域處於執行這些簡單操作的過程中的中間狀態。

  有一點很重要,那就是要注意到遞增程序本身也需要多個步驟,並且在遞增過程中任務可能會被純種機制掛起--也就是說,在Java中,遞增不是原子性的操作。因此,如果不保護任務,即使單一的遞增也不是安全的。

21.4 終結任務

21.4.3 中斷

  Executor上呼叫shutdownNow(),它將發送一個interrupt()呼叫給它啟動的所有執行緒。

  Executor 透過呼叫submit()而不是excutor()來啟動任務,就可以持有該任務的上下文。 submit()將傳回一個泛型的Future<?>,持有這種Future的關鍵在於你可以在其上呼叫 cancel(),並因此可以使用它來中斷某個特定任務。如果你將true傳遞給cancel(),那麼它就會擁有在該執行緒上呼叫interrupt()以停止這個執行緒的權限。因此,cancel()是一個種中斷由Excutor啟動的單一執行緒的方式。

  SleepBlock()是可中斷的阻塞,而IOBlockedSynchronizedBlocked是不可中斷的阻塞。上面三個類別的範例證明I/O和在synchronized區塊上的等待是不可中斷的。無論是I/O還是嘗試呼叫synchronized方法,都不需要任何InterruptedException處理器。
從關於上面三個類別的範例的輸出可以看到,你能夠中斷對sleep()的呼叫(或任何要求拋出InterruptedException的呼叫)。但是,你不能中斷試圖取得synchronized鎖定或試圖執行I/O操作的執行緒。這有點令人煩惱,特別是在米婭I/O的任務時,因為這意味著IO具有鎖住你的多執行緒程式的潛在可能性。特別是對於基於Web的程序,這更是關乎利害。

  對於這類問題,有一個略顯笨拙但是有時確實行之有效的解決方案,即關閉任務在其上發生阻塞的底層資源:

21.5 線程之間的協作

21.5.1 wait()與notifyAll()

  wait()使你可以等待某個條件發生變化,而改變這個條件超出了目前方法的控制能力。通常,這種條件將由另一個任務來改變。你肯定不想在你的任務測試這個條件的同時,不斷地進行空循環,這被稱為忙等待, 通常是一種不良的周期使用方式。因此wait()會在等等外在世界產生變化的時候將任務掛起,並且只有在notify()notifyAll() 發生時,即表示發生了某些感興趣的事物,這個任務才會被喚醒並去檢查所產生的變化。因此,wait()提供了一種在任務之間對活動同步的方式。

  呼叫sleep()的時候鎖定並沒有被 釋放,呼叫yield()也屬於這種情況,理解這一點很重要。
wait(), notify()以及notifyAll()有一個比較特殊的方面,那就是這些方法是基類Object 的一個部分,而不是屬於Thread的一部分。

  錯失的訊號。

21.5.2 notify() 與notifyAll()

  在有關Java的線程機制的討論中,有一個令人困惑的描述: notifyAll()將喚醒“所有下在等等的任務」。這是否意味著在程式中任何地方,任何處於wait()狀態中的任務都會被任何對notifyAll()的呼叫喚醒呢?有範例說明情況並非如此-事實上,當notifyAll()因某個特定鎖定而被呼叫時,只有等待這個鎖定的任務才會被喚醒。

21.6 死鎖

  由Edsger Dijkstrar提出的哲學家就餐問題是一個經典的死鎖例證。

  要修正死鎖問題,你必須明白,當以下四個條件同時滿足時,就會發生死鎖:

  • 互斥條件。任務使用的資源中至少有一個是不能共享的。這裡,一根Chopstick一次就只能被一個Philosopher使用。

  • 至少有一個任務它必須持有一個資源且正在等待取得一個目前被別的任務所持有的資源。也就是說,要發生死鎖,Philosopher必須拿著一根Chopstick並且等待另一根。

  • 資源不能被任務搶佔,任務必須把資源釋放當作普通事件。 Philosopher很有禮貌,他們不會從其他Philosopher那裡搶佔Chopstick。

  • 必須有循環等待,這時,一個任務等待其他任務所持有的資源,後者又在等待另一個任務所持有的漿,這樣一直下去,直到有一個任務在等待第一個任務所持有的資源,使得大家都被鎖住。在DeadlockingDiningPhilosophers.java中,因為每個Philosopher都試圖先得到右邊的Chopstick,然後得到左邊的Chopstick,所以發徨了循環等待。

  所以要防止死鎖的話,只需破壞其中一個即可。防止死鎖最容易的方法是破壞第4個條件。

21.7 新類別庫中的構件

21.7.1 CountDownLatch

  適用場景:它被用來同步一個或多個任務,強制它們等待由其他任務執行的一組操作完成。即一個或多個任務需要等待,等待到其它任務,例如一個問題的初始部分,完成為止。

  你可以向CountDownLatch物件設定一個初始值,任何在這個物件上呼叫wait()的方法都會阻塞,直到這個計數值到達0.其他因結束其工作時,可以在存取對像上呼叫countDown()來減少這個計數值。 CountDownLatch被設計成只解發一次,計數值不能被重置。如果你需要能夠重置計數值的版本,則可以使用CyclicBarrier

  呼叫countDown()的任務在產生這個呼叫時並沒有被阻塞,只有對await()的呼叫會被阻塞,直到計數值到達0

  CountDownLatch的典型用法是將一個程式分成n個互相獨立的可解決任務,並建立一個值為nCountDownLatch。當每個任務完成時,都會在這個鎖存器上呼叫countDown()。等待問題被解決的任務在這個鎖存器上呼叫await(),將它們自己掛起,直到鎖存器計數結束。

21.7.2 CyclicBarrier

  適用於這樣的情況:你希望創建一組任務,它們並行地執行工作,然後在進行下一下步驟之前等待,直至所有任務都完成(看起來有些像Join())。它使得所有的並行任務都將在柵欄處列隊,因此可以一致地向前移動。

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

21.7.3 DelayQueue

  DelayQueue是一個無界的BlockingQueue(同步佇列),用於放置實作了Delayed介面的對象,其中的物件只能在其到期時才能從佇列中取走。這種隊列是有順序的,即隊頭物件是最先到期的物件。如果沒有到期的對象,那麼佇列就沒有頭元素,所以poll()將返回null(也正因為此,我們不能將null放置到這種隊列中)。如上所述,DelayQueue就成為了優先權隊列的一種變體。

21.7.4 PriorityBlockingQueue

  這是一個很基礎的優先權佇列,它具有可阻塞的讀取操作。這種隊列的阻塞特性提供了所有必需的同步,所以你應該注意到了,這裡不需要任何明確的同步——不必考慮當你從這種隊列中讀取時,其中是否有元素,因為這個隊列在沒有元素時,將直接阻塞讀取者。

21.7.5 使用ScheduledExecutor的室溫控制器

  「溫室控制系統」可以被視為一種並發問題,每個期望的溫室事件都是預定時間運行的任務。
ScheduledThreadPoolExecutor可以解決這種問題。其中schedule()用來執行一次任務,scheduleAtFixedRate()每隔規定的時間重複執行任務。兩個方法接收delayTime參數。可以將Runnable物件設定為在將來的某個時刻執行。

21.7.6 Semaphre

21.8  模擬

21.8.1 銀行出納員

21.8.2 飯店模擬

#  #21.8.2 飯店模擬

#  

BlockingQueue: 同步佇列,當第一個元素為空或不可用時,執行.take()時,等待(阻塞、Blocking)。

  

SynchronousQueue

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

  關於這個範例,需要觀察的一項非常重要的事項,就是使用佇列在任務間通訊所帶來的管理複雜度。這個單項技術透過反轉控制大幅簡化了並發程式的過程:任務沒​​有直接地互相干涉,而是經由佇列互相傳送物件。接收任務將處理對象,並將其當作一個訊息來對待,而不是向它發送訊息。如果只要可能就遵循這項技術,那麼你建構出健壯的並發系統的可能性就會大大增加。

21.8.3 分發工作

21.9 效能調優(Performance Tuning)

21.9.1 比較各類互斥技術(Comparing mutex technologies)

  「微基準測試(microbenchmarking)」危險:這個術語通常指在隔離的、脫離情境環境的情況下對某個特性進行效能測試。當然,你仍然必須編寫測試來驗證諸如「Lock比synchronized更快」這樣的斷言,但是你需要在編寫這些測試的進修意識到,在編譯過程中和在運行時實際會發生什麼。


  不同的編譯器和運行時系統在這方面會有所差異,因此很難確切了解將會發生什麼,但是我們需要防止編譯器預測結果的可能性。

      使用Lock通常會比使用synchronized要高效許多,而且synchronized的開銷看起來變化範圍太大,而Lock相對比較一致。
  • 這是否意味著你永遠不應該使用synchronized關鍵字呢?這裡有兩個因素要考慮:

  • 一是互斥方法的方法體的大小。

二是synchronized關鍵字所產生的代碼與Lock所需的「加鎖-try/finally-解鎖」慣用法所產生的代碼相比,可讀性提高了很多。

  程式碼被閱讀的次數遠多於被寫的次數。在程式設計時,與其他人交流相對於與電腦交流而言,要重要得多,因此程式碼的可讀性至關重要。因此,以synchronized關鍵字入手,只有在效能調優時才替換為Lock物件這種做法,是具有實際意義的。

21.9.2 免鎖容器(Lock-free containers)

  這些免鎖視窗的通用策略是:對容器的修改可以與讀取操作同時發生,只要讀取者只能看到完成修改的結果婀。修改是在容器資料結構的某個部分的一個單獨的副本(有時是整個資料結構的副本)上執行的,並且這個副本在修改過程中是不可視的。只有當修改完成時,被修改的結構都會自動地與主資料結構交換,之後讀取者就可以看到這個修改了。

  樂觀鎖

  只要你主要是從免鎖容器中讀取,那麼它就會比其synchronized對應物快許多,因為獲取和釋放鎖的開銷被省掉了。如果需要向免鎖容器中執行少量寫入,那麼情況仍舊如此,但是什麼算「少量」?這是一個很有趣的問題。

21.11 總結

  執行緒的一個額外好處是它們提供了輕量級的執行上下文切換(大約100條指令),而不是重量級的進程上下文切換(要上千條指令)。因為一個給定進程內的所有執行緒共享相同的記憶體空間,輕量級的上下文切換只是改變了程式的執行序列和局部變數。進程切換(重量級的上下文切換)必須改變所有記憶體空間。 ######相關文章:###

Java程式設計思想學習課程時(六)第19章-枚舉類型

#Java程式設計思想學習課程時(七)第20章-註解

#

以上是Java程式設計思想學習課程(八)第21章-並發的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn