如何才能夠系統地學習Java併發技術?

a724888發表於2019-10-18

Java併發程式設計一直是Java程式設計師必須懂但又是很難懂的技術內容。

這裡不僅僅是指使用簡單的多執行緒程式設計,或者使用juc的某個類。當然這些都是併發程式設計的基本知識,除了使用這些工具以外,Java併發程式設計中涉及到的技術原理十分豐富。為了更好地把併發知識形成一個體系,也鑑於本人目前也沒有能力寫出這類文章,於是參考幾位併發程式設計方面專家的部落格和書籍,做一個簡單的整理。

首先說一下我學習Java併發程式設計的一些方法吧。大概分為這幾步:


1、先學會最基礎的Java多執行緒程式設計,Thread類的使用,執行緒通訊的一些方法等等。這部分內容需要多寫一些demo去實踐。


2、接下來可以去使用一些JUC的API,比如concurrenthashmap,併發工具類,原子資料型別等工具,在學習這部分內容的時候,你可以搭配一些介紹併發程式設計的書籍和部落格一起看,書籍我當時看的是《Java併發程式設計藝術》,我覺得略好於《Java併發程式設計實踐》。

我這個專欄裡也整合了一些比較好的部落格,所以大家可以不妨先看看。


3、接下來就要閱讀原始碼了,讀原始碼部分最主要的就是讀JUC包的原始碼,比如concurrenthashmap,阻塞佇列,執行緒池等等,當然,這些原始碼自己讀起來會比較痛苦,所以建議跟著部落格走。


4、走到這一步,你已經理解了Java併發程式設計原理,並且可以熟練使用JUC,應付面試已經足夠了,剩下的事情就是真正把這些東西用到專案中去,我當時在網易實習的時候就用到了JUC的一些內容,不得不說還是挺有意思的。


下面先介紹一下Java併發程式設計的一些主要內容,我把它分六個部分,大家可以參考這幾個部分的內容分別進行學習。

一:併發基礎和多執行緒

首先需要學習的就是併發的基礎知識,什麼是併發,為什麼要併發,多執行緒的概念,執行緒安全的概念等。

然後學會使用Java中的Thread或是其他執行緒實現方法,瞭解執行緒的狀態轉換,執行緒的方法,執行緒的通訊方式等。

二:JMM記憶體模型

任何語言最終都是執行在處理器上,JVM虛擬機器為了給開發者一個一致的程式設計記憶體模型,需要制定一套規則,這套規則可以在不同架構的機器上有不同實現,並且向上為程式設計師提供統一的JMM記憶體模型。

所以瞭解JMM記憶體模型也是瞭解Java併發原理的一個重點,其中瞭解指令重排,記憶體屏障,以及可見性原理尤為重要。

JMM只保證happens-before和as-if-serial規則,所以在多執行緒併發時,可能出現原子性,可見性以及有序性這三大問題。

下面的內容則會講述Java是如何解決這三大問題的。

三:synchronized,volatile,final等關鍵字

對於併發的三大問題,volatile可以保證可見性,synchronized三種特性都可以保證。

synchronized是基於作業系統的mutex lock指令實現的,volatile和final則是根據JMM實現其記憶體語義。

此處還要了解CAS操作,它不僅提供了類似volatile的記憶體語義,並且保證操作原子性,因為它是由硬體實現的。

JUC中的Lock底層就是使用volatile加上CAS的方式實現的。synchronized也會嘗試用cas操作來優化器重量級鎖。

瞭解這些關鍵字是很有必要的。

四:JUC包

在瞭解完上述內容以後,就可以看看JUC的內容了。

JUC提供了包括Lock,原子操作類,執行緒池,同步容器,工具類等內容。

這些類的基礎都是AQS,所以瞭解AQS的原理是很重要的。

除此之外,還可以瞭解一下Fork/Join,以及JUC的常用場景,比如生產者消費者,阻塞佇列,以及讀寫容器等。

五:實踐

上述這些內容,除了JMM部分的內容比較不好實現之外,像是多執行緒基本使用,JUC的使用都可以在程式碼實踐中更好地理解其原理。多嘗試一些場景,或者在網上找一些比較經典的併發場景,或者參考別人的例子,在實踐中加深理解,還是很有必要的。

六:補充

由於很多Java新手可能對併發程式設計沒什麼概念,在這裡放一張不錯的思維導圖,該圖簡要地提幾個併發程式設計中比要重要的點,也是比較基本的點,在大致瞭解了這些基礎內容以後,才能更好地開展後面詳細內容的學習。

上面講到了學習路線,建議大家先跟著這個路線去看一看本專欄的一些部落格,然後再來看下面這部分內容,因為下面的內容是我基於本專欄所有部落格進行歸納和總結的,主要是方便記憶和複習,也可以讓你把知識點重新過一遍,如果你覺得我的總結不夠好,你也可以自己做總結,這也是一種不錯的學習方法,話不多少,我們們接著往下看。

這篇總結主要是基於我Java併發技術系列的文章而形成的的。主要是把重要的知識點用自己的話說了一遍,可能會有一些錯誤,還望見諒和指點。謝謝

更多詳細內容可以檢視我的專欄文章:Java併發技術指南

https://blog.csdn.net/column/details/21961.html

執行緒安全

  1. 執行緒安全一般指多執行緒之間的操作結果不會因為執行緒排程的順序不同而發生改變。

互斥和同步

  1. 互斥一般指資源的獨佔訪問,同步則要求同步程式碼中的程式碼順序執行,並且也是單執行緒獨佔的。

JMM記憶體模型

  1. JVM中的記憶體分割槽包括堆,棧,方法區等區域,這些記憶體都是抽象出來的,實際上,系統中只有一個主記憶體,但是為了方便Java多執行緒語義的實現,以及降低程式設計師編寫併發程式的難度,Java提出了JMM記憶體模型,將記憶體分為主記憶體和工作記憶體,工作記憶體是執行緒獨佔的,實際上它是一系列暫存器,編譯器優化後的結果。

as-if-Serial,happens-before

  1. as if serial語義提供單執行緒程式碼的順序執行保證,雖然他允許指令重排序,但是前提是指令重排序不會改變執行結果。

volatile

  1. volatile語義實際上是在程式碼中插入一個記憶體屏障,記憶體屏障分為讀寫,寫讀,讀讀,寫寫四種,可以用來避免volatile變數的讀寫操作發生重排序,從而保證了volatile的語義,實際上,volatile修飾的變數強制要求執行緒寫時將資料從快取刷入主記憶體,讀時強制要求執行緒從主記憶體中讀取,因此保證了它的可見性。
  2. 而對於volatile修飾的64位型別資料,可以保證其原子性,不會因為指令重排序導致一個64位資料被分割成兩個32位資料來讀取。

synchronized和鎖優化

  1. synchronized是Java提供的同步標識,底層是作業系統的mutex lock呼叫,需要進行使用者態到核心態的切換,開銷比較大。

  2. synchronized經過編譯後的彙編程式碼會有monitor in和monitor out的字樣,用於標識進入監視器模組和退出監視器模組,

  3. 監視器模組watcher會監控同步程式碼塊中的執行緒號,只允執行緒號正確的執行緒進入。

  4. Java在synchronized關鍵字中進行了多次優化。

  5. 比如輕量級鎖優化,使用鎖物件的物件頭做文章,當一個執行緒需要獲得該物件鎖時,執行緒有一段空間叫做lock record,用於儲存物件頭的mask word,然後通過cas操作將物件頭的mask word改成指向執行緒中的lockrecord。

  6. 如果成功了就是獲取到了鎖,否則就是發生了互斥。需要鎖粗化,膨脹為互斥鎖。

  7. 偏向鎖,去掉了更多的同步措施,檢查mask word是否是可偏向狀態,然後檢查mask word中的執行緒id是否是自己的id,如果是則執行同步程式碼,如果不是則cas修改其id,如果修改失敗,則出現鎖爭用,偏向鎖失效,膨脹為輕量級鎖。

  8. 自旋鎖,每個執行緒會被分配一段時間片,並且聽候cpu排程,如果發生執行緒阻塞需要切換的開銷,於是使用自旋鎖不需要阻塞,而是忙等迴圈,一獲取時間片就開始忙等,這樣的鎖就是自旋鎖,一般用於併發量比較小,又擔心切換開銷的場景。

CAS操作

  1. CAS操作是通過硬體實現的原子操作,通過一條指令完成比較和賦值的操作,防止發生因指令重排導致的非原子操作,在Java中通過unsafe包可以直接使用,在Java原子類中使用cas操作來完成一系列原子資料型別的構建,保證自加自減等依賴原值的操作不會出現併發問題。

  2. cas操作也廣泛用在其他併發類中,通過迴圈cas操作可以完成執行緒安全的併發賦值,也可以通過一次cas操作來避免使用互斥鎖。

Lock類

AQS

  1. AQS是Lock類的基石,他是一個抽象類,通過操作一個變數state來判斷執行緒鎖爭用的情況,通過一系列方法實現對該變數的修改。一般可以分為獨佔鎖和互斥鎖。

  2. AQS維護著一個CLH阻塞佇列,這個佇列主要用來存放阻塞等待鎖的執行緒節點。可以看做一個連結串列。

一:獨佔鎖

獨佔鎖的state只有0和1兩種情況(如果是可重入鎖也可以把state一直往上加,這裡不討論),state = 1時說明已經有執行緒爭用到鎖。執行緒獲取鎖時一般是通過aqs的lock方法,如果state為0,首先嚐試cas修改state=1,成功返回,失敗時則加入阻塞佇列。 非公共鎖使用時,執行緒節點加入阻塞佇列時依然會嘗試cas獲取鎖,最後如果還是失敗再老老實實阻塞在佇列中。

獨佔鎖還可以分為公平鎖和非公平鎖,公平鎖要求鎖節點依據順序加入阻塞佇列,通過判斷前置節點的狀態來改變後置節點的狀態,比如前置節點獲取鎖後,釋放鎖時會通知後置節點。

非公平鎖則不一定會按照佇列的節點順序來獲取鎖,如上面所說,會先嚐試cas操作,失敗再進入阻塞佇列。

二:共享鎖

共享鎖的state狀態可以是0到n。共享鎖維護的阻塞佇列和互斥鎖不太一樣,互斥鎖的節點釋放鎖後只會通知後置節點,而共享鎖獲取鎖後會通知所有的共享型別節點,讓他們都來獲取鎖。共享鎖用於countdownlatch工具類與cyliderbarrier等,可以很好地完成多執行緒的協調工作

鎖Lock和Conditon

Lock 鎖維護這兩個內部類fairsync和unfairsync,都繼承自aqs,重寫了部分方法,實際上大部分方法還是aqs中的,Lock只是重新把AQS做了封裝,讓程式設計師更方便地使用Lock鎖。

和Lock鎖搭配使用的還有condition,由於Lock鎖只維護著一個阻塞佇列,有時候想分不同情況進行鎖阻塞和鎖通知怎麼辦,原來我們一般會使用多個鎖物件,現在可以使用condition來完成這件事,比如執行緒A和執行緒B分別等待事件A和事件B,可以使用兩個condition分別維護兩個佇列,A放在A佇列,B放在B佇列,由於Lock和condition是繫結使用的,當事件A觸發,執行緒A被喚醒,此時他會加入Lock自己的CLH佇列中進行鎖爭用,當然也分為公平鎖和非公平鎖兩種,和上面的描述一樣。

Lock和condtion的組合廣泛用於JUC包中,比如生產者和消費者模型,再比如cyliderbarrier。

讀寫鎖

讀寫鎖也是Lock的一個子類,它在一個阻塞佇列中同時儲存讀執行緒節點和寫執行緒節點,讀寫鎖採用state的高16位和低16位分別代表獨佔鎖和共享鎖的狀態,如果共享鎖的state > 0可以繼續獲取讀鎖,並且state-1,如果=0,則加入到阻塞佇列中,寫鎖節點和獨佔鎖的處理一樣,因此一個佇列中會有兩種型別的節點,喚醒讀鎖節點時不會喚醒寫鎖節點,喚醒寫鎖節點時,則會喚醒後續的節點。

因此讀寫鎖一般用於讀多寫少的場景,寫鎖可以降級為讀鎖,就是在獲取到寫鎖的情況下可以再獲取讀鎖。

併發工具類

1 countdownlatch

countdownlatch主要通過AQS的共享模式實現,初始時設定state為N,N是countdownlatch初始化使用的size,每當有一個執行緒執行countdown,則state-1,state = 0之前所有執行緒阻塞在佇列中,當state=0時喚醒隊頭節點,隊頭節點依次通知所有共享型別的節點,喚醒這些執行緒並執行後面的程式碼。

2 cycliderbarrier

cycliderbarrier主要通過lock和condition結合實現,首先設定state為屏障等待的執行緒數,在某個節點設定一個屏障,所有執行緒執行到此處會阻塞等待,其實就是等待在一個condition的佇列中,並且每當有一個執行緒到達,state -=1 則當所有執行緒到達時,state = 0,則喚醒condition佇列的所有結點,去執行後面的程式碼。

3 samphere

samphere也是使用AQS的共享模式實現的,與countlatch大同小異,不再贅述。

4 exchanger

exchanger就比較複雜了。使用exchanger時會開闢一段空間用來讓兩個執行緒進行互動操作,這個空間一般是一個棧或佇列,一個執行緒進來時先把資料放到這個格子裡,然後阻塞等待其他執行緒跟他交換,如果另一個執行緒也進來了,就會讀取這個資料,並把自己的資料放到對方執行緒的格子裡,然後雙雙離開。當然使用棧和佇列的互動是不同的,使用棧的話匹配的是最晚進來的一個執行緒,佇列則相反。

原子資料型別

原子資料型別基本都是通過cas操作實現的,避免併發操作時出現的安全問題。

同步容器

同步容器主要就是concurrenthashmap了,在集合類中我已經講了chm了,所以在這裡簡單帶過,chm1.7通過分段鎖來實現鎖粗化,使用的死LLock鎖,而1.8則改用synchronized和cas的結合,效能更好一些。

還有就是concurrentlinkedlist,ConcurrentSkipListMap與CopyOnWriteArrayList。

第一個連結串列也是通過cas和synchronized實現。

而concurrentskiplistmap則是一個跳錶,跳錶分為很多層,每層都是一個連結串列,每個節點可以有向下和向右兩個指標,先通過向右指標進行索引,再通過向下指標細化搜尋,這個的搜尋效率是很高的,可以達到logn,並且它的實現難度也比較低。通過跳錶存map就是把entry節點放在連結串列中了。查詢時按照跳錶的查詢規則即可。

CopyOnWriteArrayList是一個寫時複製連結串列,查詢時不加鎖,而修改時則會複製一個新list進行操作,然後再賦值給原list即可。 適合讀多寫少的場景。

阻塞佇列

BlockingQueue 實現之 ArrayBlockingQueue

  1. ArrayBlockingQueue其實就是陣列實現的阻塞佇列,該阻塞佇列通過一個lock和兩個condition實現,一個condition負責從隊頭插入節點,一個condition負責隊尾讀取節點,通過這樣的方式可以實現生產者消費者模型。

BlockingQueue 實現之 LinkedBlockingQueue

  1. LinkedBlockingQueue是用連結串列實現的阻塞佇列,和arrayblockqueue有所區別,它支援實現為無界佇列,並且它使用兩個lock和對應的condition搭配使用,這是因為連結串列可以同時對頭部和尾部進行操作,而陣列進行操作後可能還要執行移位和擴容等操作。

  2. 所以連結串列實現更靈活,讀寫分別用兩把鎖,效率更高。

BlockingQueue 實現之 SynchronousQueue

  1. SynchronousQueue實現是一個不儲存資料的佇列,只會保留一個佇列用於儲存執行緒節點。詳細請參加上面的exchanger實現類,它就是基於SynchronousQueue設計出來的工具類。

BlockingQueue 實現之 PriorityBlockingQueue

PriorityBlockingQueue

  1. PriorityBlockingQueue是一個支援優先順序的無界佇列。預設情況下元素採取自然順序排列,也可以通過比較器comparator來指定元素的排序規則。元素按照升序排列。

DelayQueue

  1. DelayQueue是一個支援延時獲取元素的無界阻塞佇列。佇列使用PriorityQueue來實現。佇列中的元素必須實現Delayed介面,在建立元素時可以指定多久才能從佇列中獲取當前元素。只有在延遲期滿時才能從佇列中提取元素。我們可以將DelayQueue運用在以下應用場景:

  2. 快取系統的設計:可以用DelayQueue儲存快取元素的有效期,使用一個執行緒迴圈查詢DelayQueue,一旦能從DelayQueue中獲取元素時,表示快取有效期到了。

  3. 定時任務排程。使用DelayQueue儲存當天將會執行的任務和執行時間,一旦從DelayQueue中獲取到任務就開始執行,從比如TimerQueue就是使用DelayQueue實現的。

執行緒池

類圖

首先看看executor介面,只提供一個run方法,而他的一個子介面executorservice則提供了更多方法,比如提交任務,結束執行緒池等。

然後抽象類abstractexecutorservice提供了更多的實現了,最後我們最常使用的類ThreadPoolExecutor就是繼承它來的。

ThreadPoolExecutor可以傳入多種引數來自定義實現執行緒池。

而我們也可以使用Executors中的工廠方法來例項化常用的執行緒池。

常用執行緒池

比如newFixedThreadPool

newSingleThreadExecutor newCachedThreadPool

newScheduledThreadPool等等,這些執行緒池即可以使用submit提交有返回結果的callable和futuretask任務,通過一個future來接收結果,或者通過callable中的回撥函式call來回寫執行結果。也可以用execute執行無返回值的runable任務。

在探討這些執行緒池的區別之前,先看看執行緒池的幾個核心概念。

1 任務佇列:執行緒池中維護了一個任務佇列,每當向執行緒池提交任務時,任務加入佇列。

2 工作執行緒:也叫worker,從執行緒池中獲取任務並執行,執行後被回收或者保留,因情況而定。

3 核心執行緒數和最大執行緒數,核心執行緒數是執行緒池需要保持存活的執行緒數量,以便接收任務,最大執行緒數是能建立的執行緒數上限。

4 newFixedThreadPool可以設定固定的核心執行緒數和最大執行緒數,一個任務進來以後,就會開啟一個執行緒去執行,並且這部分執行緒不會被回收,當開啟的執行緒達到核心執行緒數時,則把任務先放進任務佇列。當任務佇列已滿時,才會繼續開啟執行緒去處理,如果執行緒總數打到最大執行緒數限制,任務佇列又是滿的時候,會執行對應的拒絕策略。

5 拒絕策略一般有幾種常用的,比如丟棄任務,丟棄隊尾任務,回退給呼叫者執行,或者丟擲異常,也可以使用自定義的拒絕策略。

6 newSingleThreadExecutor是一個單執行緒執行的執行緒池,只會維護一個執行緒,他也有任務佇列,當任務佇列已滿並且執行緒數已經是1個的時候,再提交任務就會執行拒絕策略。

7 newCachedThreadPool比較特別,第一個任務進來時會開啟一個執行緒,而後如果執行緒還沒執行完前面的任務又有新任務進來,就會再建立一個執行緒,這個執行緒池使用的是無容量的SynchronousQueue佇列,要求請求執行緒和接受執行緒匹配時才會完成任務執行。 所以如果一直提交任務,而接受執行緒來不及處理的話,就會導致執行緒池不斷建立執行緒,導致cpu消耗很大。

8 ScheduledThreadPoolExecutor內部使用的是delayqueue佇列,內部是一個優先順序佇列priorityqueue,也就是一個堆。通過這個delayqueue可以知道執行緒排程的先後順序和執行時間點。

Fork/Join框架

又稱工作竊取執行緒池。

我們在大學演算法課本上,學過的一種基本演算法就是:分治。其基本思路就是:把一個大的任務分成若干個子任務,這些子任務分別計算,最後再Merge出最終結果。這個過程通常都會用到遞迴。

而Fork/Join其實就是一種利用多執行緒來實現“分治演算法”的並行框架。

另外一方面,可以把Fori/Join看作一個單機版的Map/Reduce,只不過這裡的並行不是多臺機器平行計算,而是多個執行緒平行計算。

1 與ThreadPool的區別 通過上面例子,我們可以看出,它在使用上,和ThreadPool有共同的地方,也有區別點: (1) ThreadPool只有“外部任務”,也就是呼叫者放到佇列裡的任務。 ForkJoinPool有“外部任務”,還有“內部任務”,也就是任務自身在執行過程中,分裂出”子任務“,遞迴,再次放入佇列。 (2)ForkJoinPool裡面的任務通常有2類,RecusiveAction/RecusiveTask,這2個都是繼承自FutureTask。在使用的時候,重寫其compute演算法。

2 工作竊取演算法 上面提到,ForkJoinPool裡有”外部任務“,也有“內部任務”。其中外部任務,是放在ForkJoinPool的全域性佇列裡面,而每個Worker執行緒,也有一個自己的佇列,用於存放內部任務。

3 竊取的基本思路就是:當worker自己的任務佇列裡面沒有任務時,就去scan別的執行緒的佇列,把別人的任務拿過來執行

微信公眾號

個人公眾號:黃小斜

黃小斜是跨考軟體工程的 985 碩士,自學 Java 兩年,拿到了 BAT 等近十家大廠 offer,從技術小白成長為阿里工程師。

作者專注於 JAVA 後端技術棧,熱衷於分享程式設計師乾貨、學習經驗、求職心得和程式人生,目前黃小斜的CSDN部落格有百萬+訪問量,知乎粉絲2W+,全網已有10W+讀者。

黃小斜是一個斜槓青年,堅持學習和寫作,相信終身學習的力量,希望和更多的程式設計師交朋友,一起進步和成長!

原創電子書:
關注公眾號【黃小斜】後回覆【原創電子書】即可領取我原創的電子書《菜鳥程式設計師修煉手冊:從技術小白到阿里巴巴Java工程師》

程式設計師3T技術學習資源: 一些程式設計師學習技術的資源大禮包,關注公眾號後,後臺回覆關鍵字 “資料” 即可免費無套路獲取。

考研複習資料:
計算機考研大禮包,都是我自己考研複習時用的一些複習資料,包括公共課和專業的複習視訊,這裡也推薦給大家,關注公眾號後,後臺回覆關鍵字 “考研” 即可免費獲取。

技術公眾號:Java技術江湖

如果大家想要實時關注我更新的文章以及分享的乾貨的話,可以關注我的公眾號【Java技術江湖】一位阿里 Java 工程師的技術小站,作者黃小斜,專注 Java 相關技術:SSM、SpringBoot、MySQL、分散式、中介軟體、叢集、Linux、網路、多執行緒,偶爾講點Docker、ELK,同時也分享技術乾貨和學習經驗,致力於Java全棧開發!

Java工程師必備學習資源: 一些Java工程師常用學習資源,關注公眾號後,後臺回覆關鍵字 “Java” 即可免費無套路獲取。

我的公眾號

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69906029/viewspace-2660581/,如需轉載,請註明出處,否則將追究法律責任。

相關文章