1. 程式人生 > >簡述併發程式設計分為三個核心問題:分工、同步、互斥。

簡述併發程式設計分為三個核心問題:分工、同步、互斥。

    總的來說,併發程式設計可以總結為三個核心問題:分工、同步、互斥。

    所謂分工指的是如何高效地拆解任務並分配給執行緒,而同步指的是執行緒之間如何協作,互斥則是保證同一時刻只允許一個執行緒訪問共享資源。java SDK併發包很大部分內容都是按照這三個維度組織的,例如Fork/Join框架就是一種分工模式,CountDownLatch就是一種典型的同步方式,而可重入鎖則是一種互斥手段。

    1.分工

    所謂分工,類似於現實中一個組織完成一個專案,專案經理要拆分任務,安排合適的成員去完成。

    在併發程式設計領域,你就是專案經理,執行緒就是專案組成員。任務分解和分工對於專案成敗非常關鍵,不過在併發領域裡,分工更重要,它直接決定了併發程式的效能。在現實世界裡,分工是很複雜的,著名數學家華羅庚曾用“燒水泡茶”的例子通俗地講解了統籌方法(一種安排工作程序的數學方法),“燒水泡茶”這麼簡單的事情都這麼多說道,更何況是併發程式設計裡的工程問題呢。

    既然分工很重要又很複雜,那一定有前輩努力嘗試解決過,並且也一定有成果。的確,在併發程式設計領域這方面的成果還是很豐碩的。Java SDK併發包裡的Executor、Fork/Join、Future本質上就是一種分工方法。除此之外,併發程式設計領域還總結了一些設計模式,基本上都是和分工方法相關的,例如生產者-消費者、Thread-Per-Message、Worker Thread模式等都是用來指導你如何分工的。

    瞭解這部分內容,最佳的方式就是和現實世界做對比。例如生產者-消費者模式,可以類比一下餐館裡的大廚和服務員,大廚就是生產者,負責做菜,做完放到出菜口,而服務員就是消費者,把做好的菜給你端過來。不過,我們經常會發現,出菜口有時候一下子出了好幾個菜,服務員是可以吧這一批菜同時端給你的。其實這就是生產者-消費者模式的一個優點,生產者一個一個地生產資料,而消費者可以批處理,這樣就提高了效能。

    2.同步

    分好工之後,就是具體執行了。在專案執行過程中,任務之間是有依賴的,一個任務結束後,依賴它的後續任務任務就可以開工了,後續工作怎麼知道可以開工了呢?這個就是靠溝通協作了,這是一項很重要的工作。

    在併發程式設計領域裡的同步,主要指的就是執行緒間的協作,本質上和現實生活中的協作沒區別,不過是一個執行緒執行完了一個任務,如何通知執行後續任務的執行緒開工而已。協作一般是和分工相關的。Java SDK併發包裡的Executor、Fork/Join、Future本質上都是分工方法,但同時也能解決執行緒協作的問題。例如,用Future可以發起一個非同步呼叫,當主執行緒通過get()方法取結果時,主執行緒就會等待,當非同步執行的結果返回時,get()方法就自動返回了。主執行緒和非同步執行緒之間的協作,Future工具類已經幫我們解決了。除此之外,Java SDK裡提供的CountDownLatch、CyclicBarrier、Phaser、Exchanger也都是用來解決執行緒協作的問題。

    不過還有很多場景,是需要你自己來處理執行緒之間的協作的。

    工作中遇到的執行緒協作的問題,基本上都可以描述為這樣的一個問題:當某個條件不滿足時,執行緒需要等待,當某個條件滿足時,執行緒需要被喚醒執行。例如,在生產者-消費者模型裡,也有類似的描述,“當佇列滿時,生產者執行緒等待,當佇列不滿時,生產者執行緒需要被喚醒執行;當佇列空時,消費者執行緒等待,當佇列不為空時,消費者執行緒需要被喚醒執行。”

    在Java併發程式設計領域,解決協作問題的核心技術是管程,上面提到的所有執行緒協作技術底層都是利用管程解決的。管程是一種解決併發問題的通用模型,除了能解決執行緒協作問題,還能解決下面我們將要介紹的互斥問題。可以這麼說,管程是解決併發問題的萬能鑰匙。

    所以說,這部分內容的學習,關鍵是理解管程模型,學好它就可以解決所有問題。其次,瞭解Java SDK併發包提供的幾個執行緒協作的工具類的應用場景,用好它們可以妥妥地提高你的工作效率。

    3.互斥

    分工、同步主要強調的是效能,但併發程式裡還有一部分是關於正確性的,用專業術語叫“執行緒安全”。併發程式裡,當多個執行緒同時訪問一個共享變數時,結果時不確定的。不確定,則意味著可能正確,也可能錯誤,事先是不知道的。而導致不確定的主要源頭是可見性的問題、有序性問題和原子性問題,為了解決這三個問題,Java語言引入了記憶體模型,記憶體模型提供了一系列的規則,利用這些規則,我們可以避免可見性問題、有序性問題,但是還不足以完全解決執行緒安全問題。解決執行緒安全問題的核心方案還是互斥。

    所謂互斥,指的是同一時刻,只允許一個執行緒訪問共享變數。

    實現互斥的核心技術就是鎖,Java語言裡synchronized、SDK裡的各種Lock都能解決互斥問題。雖說鎖解決了安全性問題,但同時也帶來了效能問題,那如何保證安全性的同時又儘量提高效能呢?可以分場景優化,Java SDK裡提供的ReadWriteLock、StampedLock就可以優化讀多寫少場景下鎖的效能。還可以使用無鎖的資料結構,例如Java SDK裡提供的原子類都是基於無鎖技術實現的。

    除此之外,還有一些其他的方案,原理是不共享變數或者變數只允許讀。這方面,Java 提供了Thread Local和final關鍵字,還有一種copy-on-write的模式。

    使用鎖除了要注意效能問題外,還需要注意死鎖問題。

    這部分內容比較複雜,往往還是跨領域的,例如要理解可見性,就需要了解一些CPU和快取的知識;要理解原子性,就需要理解一些作業系統的知識;很多無鎖演算法的實現往往也需要理解CPU快取。這部分內容,需要博覽群書,在大腦裡建立起CPU、記憶體、I/O執行的模擬器。這樣遇到問題就