併發編程之入門

前言

​ 併發編程並非一門相對獨立的學科,而是一個綜合學科。併發編程相關的概念和技術看上很是零散,相關度也很低,總給你一種這樣的感受:我已經學習不少相關技術了,可仍是搞不定併發編程。那如何才能學習好併發編程呢?算法

其實很簡單,只要你能從兩個方面突破一下就能夠了。一個是「跳出來,看全景」,另外一個是「鑽進去,看本質」。編程

跳出來,看全景

​ 咱們先說「跳出來」。你應該也知道,學習最忌諱的就是「盲人摸象」,只看到局部,而沒有看到全局。因此,你須要從一個個單一的知識和技術中「跳出來」,高屋建瓴地看併發編程。固然,這首要之事就是你創建起一張全景圖。設計模式

​ 不過,併發編程相關的知識和技術還真是錯綜複雜,時至今日也尚未一張廣泛承認的全景圖,也許這正是不少人在併發編程方面難以突破的緣由吧。好在通過多年摸爬滾打,我本身已經「勾勒」出了一張全景圖,不必定科學,可是在某種程度上我想它仍是能夠指導你學好併發編程的。緩存

在我看來,併發編程領域能夠抽象成三個核心問題:分工、同步和互斥。安全

分工

​ 所謂分工,相似於現實中一個組織完成一個項目,項目經理要拆分任務,安排合適的成員去完成。數據結構

​ 在併發編程領域,你就是項目經理,線程就是項目組成員。任務分解和分工對於項目成敗很是關鍵,不過在併發領域裏,分工更重要,它直接決定了併發程序的性能。在現實世界裏,分工是很複雜的,著名數學家華羅庚曾用「燒水泡茶」的例子通俗地講解了統籌方法(一種安排工做進程的數學方法),「燒水泡茶」這麼簡單的事情都這麼多說道,更況且是併發編程裏的工程問題呢。併發

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

​ 學習這部份內容,最佳的方式就是和現實世界作對比。例如生產者 - 消費者模式,能夠類比一下餐館裏的大廚和服務員,大廚就是生產者,負責作菜,作完放到出菜口,而服務員就是消費者,把作好的菜給你端過來。不過,咱們常常會發現,出菜口有時候一會兒出了好幾個菜,服務員是能夠把這一批菜同時端給你的。其實這就是生產者 - 消費者模式的一個優勢,生產者一個一個地生產數據,而消費者能夠批處理,這樣就提升了性能。工具

同步

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

​ 在併發編程領域裏的同步,主要指的就是線程間的協做,本質上和現實生活中的協做沒區別,不過是一個線程執行完了一個任務,如何通知執行後續任務的線程開工而已。

​ 協做通常是和分工相關的。Java SDK 併發包裏的 Executor、Fork/Join、Future 本質上都是分工方法,但同時也能解決線程協做的問題。例如,用 Future 能夠發起一個異步調用,當主線程經過 get() 方法取結果時,主線程就會等待,當異步執行的結果返回時,get() 方法就自動返回了。主線程和異步線程之間的協做,Future 工具類已經幫咱們解決了。除此以外,Java SDK 裏提供的 CountDownLatch、CyclicBarrier、Phaser、Exchanger 也都是用來解決線程協做問題的。

​ 工做中遇到的線程協做問題,基本上均可以描述爲這樣的一個問題:當某個條件不知足時,線程須要等待,當某個條件知足時,線程須要被喚醒執行。例如,在生產者 - 消費者模型裏,也有相似的描述,「當隊列滿時,生產者線程等待,當隊列不滿時,生產者線程須要被喚醒執行;當隊列空時,消費者線程等待,當隊列不空時,消費者線程須要被喚醒執行。」

​ 在 Java 併發編程領域,解決協做問題的核心技術是管程,上面提到的全部線程協做技術底層都是利用管程解決的。管程是一種解決併發問題的通用模型,除了能解決線程協做問題,還能解決下面咱們將要介紹的互斥問題。能夠這麼說,管程是解決併發問題的萬能鑰匙。

因此說,這部份內容的學習,關鍵是理解管程模型,學好它就能夠解決全部問題。其次是瞭解 Java SDK 併發包提供的幾個線程協做的工具類的應用場景,用好它們能夠妥妥地提升你的工做效率。

互斥

​ 分工、同步主要強調的是性能,但併發程序裏還有一部分是關於正確性的,用專業術語叫「線程安全」。併發程序裏,當多個線程同時訪問同一個共享變量的時候,結果是不肯定的。不肯定,則意味着可能正確,也可能錯誤,事先是不知道的。而致使不肯定的主要源頭是可見性問題、有序性問題和原子性問題,爲了解決這三個問題,Java 語言引入了內存模型,內存模型提供了一系列的規則,利用這些規則,咱們能夠避免可見性問題、有序性問題,可是還不足以徹底解決線程安全問題。解決線程安全問題的核心方案仍是互斥。

​ 所謂互斥,指的是同一時刻,只容許一個線程訪問共享變量。

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

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

​ 使用鎖除了要注意性能問題外,還須要注意死鎖問題。

​ 這部份內容比較複雜,每每仍是跨領域的,例如要理解可見性,就須要瞭解一些 CPU 和緩存的知識;要理解原子性,就須要理解一些操做系統的知識;不少無鎖算法的實現每每也須要理解 CPU 緩存。這部份內容的學習,須要博覽羣書,在大腦裏創建起 CPU、內存、I/O 執行的模擬器。這樣遇到問題就能駕輕就熟了。

跳出來,看全景,可讓你的知識成體系,所學知識也融匯貫通起來,由點成線,由線及面,畫出本身的知識全景圖。

1552226930428

相關文章
相關標籤/搜索