微信公衆號【Java技術江湖】一位阿里Java工程師的技術小站html
Java併發編程一直是Java程序員必須懂但又是很難懂的技術內容。程序員
這裏不只僅是指使用簡單的多線程編程,或者使用juc的某個類。固然這些都是併發編程的基本知識,除了使用這些工具之外,Java併發編程中涉及到的技術原理十分豐富。爲了更好地把併發知識造成一個體系,也鑑於本人目前也沒有能力寫出這類文章,因而參考幾位併發編程方面專家的博客和書籍,作一個簡單的整理。面試
首先說一下我學習Java併發編程的一些方法吧。大概分爲這幾步:算法
一、先學會最基礎的Java多線程編程,Thread類的使用,線程通訊的一些方法等等。這部份內容須要多寫一些demo去實踐。編程
二、接下來能夠去使用一些JUC的API,好比concurrenthashmap,併發工具類,原子數據類型等工具,在學習這部份內容的時候,你能夠搭配一些介紹併發編程的書籍和博客一塊兒看,書籍我當時看的是《Java併發編程藝術》,我以爲略好於《Java併發編程實踐》。設計模式
我這個專欄裏也整合了一些比較好的博客,因此你們能夠不妨先看看。數組
三、接下來就要閱讀源碼了,讀源碼部分最主要的就是讀JUC包的源碼,好比concurrenthashmap,阻塞隊列,線程池等等,固然,這些源碼本身讀起來會比較痛苦,因此建議跟着博客走。 緩存
四、走到這一步,你已經理解了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/...
一:獨佔鎖
獨佔鎖的state只有0和1兩種狀況(若是是可重入鎖也能夠把state一直往上加,這裏不討論),state = 1時說明已經有線程爭用到鎖。線程獲取鎖時通常是經過aqs的lock方法,若是state爲0,首先嚐試cas修改state=1,成功返回,失敗時則加入阻塞隊列。非公共鎖使用時,線程節點加入阻塞隊列時依然會嘗試cas獲取鎖,最後若是仍是失敗再老老實實阻塞在隊列中。獨佔鎖還能夠分爲公平鎖和非公平鎖,公平鎖要求鎖節點依據順序加入阻塞隊列,經過判斷前置節點的狀態來改變後置節點的狀態,好比前置節點獲取鎖後,釋放鎖時會通知後置節點。
非公平鎖則不必定會按照隊列的節點順序來獲取鎖,如上面所說,會先嚐試cas操做,失敗再進入阻塞隊列。
二:共享鎖
共享鎖的state狀態能夠是0到n。共享鎖維護的阻塞隊列和互斥鎖不太同樣,互斥鎖的節點釋放鎖後只會通知後置節點,而共享鎖獲取鎖後會通知全部的共享類型節點,讓他們都來獲取鎖。共享鎖用於countdownlatch工具類與cyliderbarrier等,能夠很好地完成多線程的協調工做
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,則加入到阻塞隊列中,寫鎖節點和獨佔鎖的處理同樣,所以一個隊列中會有兩種類型的節點,喚醒讀鎖節點時不會喚醒寫鎖節點,喚醒寫鎖節點時,則會喚醒後續的節點。
所以讀寫鎖通常用於讀多寫少的場景,寫鎖能夠降級爲讀鎖,就是在獲取到寫鎖的狀況下能夠再獲取讀鎖。
countdownlatch主要經過AQS的共享模式實現,初始時設置state爲N,N是countdownlatch初始化使用的size,每當有一個線程執行countdown,則state-1,state = 0以前全部線程阻塞在隊列中,當state=0時喚醒隊頭節點,隊頭節點依次通知全部共享類型的節點,喚醒這些線程並執行後面的代碼。
cycliderbarrier主要經過lock和condition結合實現,首先設置state爲屏障等待的線程數,在某個節點設置一個屏障,全部線程運行到此處會阻塞等待,其實就是等待在一個condition的隊列中,而且每當有一個線程到達,state -=1 則當全部線程到達時,state = 0,則喚醒condition隊列的全部結點,去執行後面的代碼。
samphere也是使用AQS的共享模式實現的,與countlatch大同小異,再也不贅述。
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
BlockingQueue 實現之 LinkedBlockingQueue
BlockingQueue 實現之 SynchronousQueue
BlockingQueue 實現之 PriorityBlockingQueue
PriorityBlockingQueue
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能夠知道線程調度的前後順序和執行時間點。
又稱工做竊取線程池。
咱們在大學算法課本上,學過的一種基本算法就是:分治。其基本思路就是:把一個大的任務分紅若干個子任務,這些子任務分別計算,最後再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別的線程的隊列,把別人的任務拿過來執行
▼更多精彩內容這些喜聞樂見的Java面試知識點,你都掌握了嗎?
有關秋招面試的一些小技巧
設計模式常見面試知識點總結
Java基礎知識點總結
Java集合類常見面試知識點總結
[](http://mp.weixin.qq.com/s?__b...
做者黃小斜,斜槓青年,某985碩士,阿里研發工程師,於2018 年秋招拿到 BAT 頭條、網易、滴滴等 8 個大廠 offer
我的擅長領域 :自學編程、技術校園招聘、軟件工程考研(關注公衆號後回覆」資料「便可領取 3T 免費技術學習資源)