突擊併發編程JUC系列演示代碼地址:
https://github.com/mtcarpenter/JavaTutorial
CAS(Compare And Swap)
指比較並交換。CAS
算法CAS(V, E, N)
包含 3 個參數,V 表示要更新的變量,E 表示預期的值,N 表示新值。在且僅在 V 值等於 E值時,纔會將 V 值設爲 N,若是 V 值和 E 值不一樣,則說明已經有其餘線程作了更新,當前線程什麼都不作。最後,CAS 返回當前 V 的真實值。Concurrent
包下全部類底層都是依靠CAS
操做來實現,而sun.misc.Unsafe
爲咱們提供了一系列的CAS
操做。前端
ABA
問題什麼是 ABA 問題呢?多線程環境下。線程 1 從內存的V位置取出 A ,線程 2 也從內存中取出 A,並將 V 位置的數據首先修改成 B,接着又將 V 位置的數據修改成 A,線程 1 在進行CAS
操做時會發如今內存中仍然是 A,線程 1 操做成功。儘管從線程 1 的角度來講,CAS
操做是成功的,但在該過程當中其實 V 位置的數據發生了變化,線程 1 沒有感知到罷了,這在某些應用場景下可能出現過程數據不一致的問題。java
能夠版本號(version)來解決 ABA 問題的,在 atomic
包中提供了 AtomicStampedReference
這個類,它是專門用來解決 ABA 問題的。git
直達連接: AtomicStampedReference ABA 案例連接
因爲單次 CAS
不必定能執行成功,因此 CAS
每每是配合着循環來實現的,有的時候甚至是死循環,不停地進行重試,直到線程競爭不激烈的時候,才能修改爲功。程序員
CPU 資源也是一直在被消耗的,這會對性能產生很大的影響。因此這就要求咱們,要根據實際狀況來選擇是否使用 CAS
,在高併發的場景下,一般 CAS
的效率是不高的。github
不能靈活控制線程安全的範圍。只能針對某一個,而不是多個共享變量的,不能針對多個共享變量同時進行 CAS
操做,由於這多個變量之間是獨立的,簡單的把原子操做組合到一塊兒,並不具有原子性。算法
AbstractQueuedSynchronizer
抽象同步隊列簡稱AQS
,它是實現同步器的基礎組件,併發包中鎖的底層就是使用AQS
實現的。AQS
定義了一套多線程訪問共享資源的同步框架,許多同步類的實現都依賴於它,例如經常使用的Synchronized
、ReentrantLock
、ReentrantReadWriteLock
、Semaphore
、CountDownLatch
等。該框架下的鎖會先嚐試以CAS
樂觀鎖去獲取鎖,若是獲取不到,則會轉爲悲觀鎖(如RetreenLock
)。編程
ReentrantLock
。Semaphore
和CountDownLatch
。Java
從 JDK1.5
開始提供了 java.util.concurrent.atomic
包,方便程序員在多線程環 境下,無鎖的進行原子操做。在 Atomic
包裏一共有 12 個類,四種原子更新方式,分別是原子更新基本類型
,原子更新數組
,原子更新引用
和原子更新字段
。在 JDK 1.8
以後又新增幾個原子類。以下如:
後端
針對思惟導圖知識點在前面的章節都進行了理論+實踐的講解,到達地址以下:數組
突擊併發編程JUC系列-原子更新AtomicLong<br/>
突擊併發編程JUC系列-數組類型AtomicLongArray<br/>
突擊併發編程JUC系列-原子更新字段類AtomicStampedReference<br/>
突擊併發編程JUC系列-JDK1.8 擴展類型 LongAdder
AtomicLong
的經常使用方法long getAndIncrement()
:以原子方式將當前值加1,注意,返回的是舊值。(i++)long incrementAndGet()
:以原子方式將當前值加1,注意,返回的是新值。(++i)long getAndDecrement()
:以原子方式將當前值減 1,注意,返回的是舊值 。(i--)long decrementAndGet()
:以原子方式將當前值減 1,注意,返回的是新值 。(--i)long addAndGet(int delta)
:以原子方式將輸入的數值與實例中的值(AtomicLong
裏的value
)相加,並返回結果synchronized
的使用範圍要普遍得多。好比說 synchronized 既能夠修飾一個方法,又能夠修飾一段代碼,至關於能夠根據咱們的須要,很是靈活地去控制它的應用範圍synchronized
鎖的粒度都要大於原子變量的粒度。synchronized
是一種典型的悲觀鎖,而原子類偏偏相反,它利用的是樂觀鎖。volatile
可見性問題有了更高效的 LongAdder
,那 AtomicLong
能否不使用了呢?是否凡是用到 AtomicLong
的地方,均可以用 LongAdder
替換掉呢?答案是否是的,這須要區分場景。緩存
LongAdder
只提供了 add
、increment
等簡單的方法,適合的是統計求和計數的場景,場景比較單一,而 AtomicLong
還具備 compareAndSet
等高級方法,能夠應對除了加減以外的更復雜的須要 CAS
的場景。
結論:若是咱們的場景僅僅是須要用到加和減操做的話,那麼能夠直接使用更高效的 LongAdder
,但若是咱們須要利用 CAS
好比compareAndSet
等操做的話,就須要使用 AtomicLong
來完成。
直達連接: 突擊併發編程JUC系列-JDK1.8 擴展類型 LongAdder
CountDownLatch
基於線程計數器來實現併發訪問控制,主要用於主線程等待其餘子線程都執行完畢後執行相關操做。其使用過程爲:在主線程中定義CountDownLatch
,並將線程計數器的初始值設置爲子線程的個數,多個子線程併發執行,每一個子線程在執行完畢後都會調用countDown
函數將計數器的值減1,直到線程計數器爲0,表示全部的子線程任務都已執行完畢,此時在CountDownLatch
上等待的主線程將被喚醒並繼續執行。
突擊併發編程JUC系列-併發工具 CountDownLatch
CyclicBarrier
(循環屏障)是一個同步工具,能夠實現讓一組線程等待至某個狀態以後再所有同時執行。在全部等待線程都被釋放以後,CyclicBarrier
能夠被重用。CyclicBarrier
的運行狀態叫做Barrier
狀態,在調用await
方法後,線程就處於Barrier
狀態。
CyclicBarrier
中最重要的方法是await方法,它有兩種實現。
public int await()
:掛起當前線程直到全部線程都爲Barrier狀態再同時執行後續的任務。public int await(long timeout, TimeUnit unit)
:設置一個超時時間,在超時時間事後,若是還有線程未達到Barrier
狀態,則再也不等待,讓達到Barrier狀態的線程繼續執行後續的任務。突擊併發編程JUC系列-併發工具 CyclicBarrier
Semaphore
指信號量,用於控制同時訪問某些資源的線程個數,具體作法爲經過調用acquire()
獲取一個許可,若是沒有許可,則等待,在許可以使用完畢後經過release()
釋放該許可,以便其餘線程使用。
突擊併發編程JUC系列-併發工具 Semaphore
相同點:都能阻塞一個或一組線程,直到某個預設的條件達成發生,再統一出發。
可是它們也有不少不一樣點,具體以下。
CyclicBarrier
要等固定數量的線程都到達了柵欄位置才能繼續執行,而 CountDownLatch
只需等待數字倒數到 0,也就是說 CountDownLatch
做用於事件,但 CyclicBarrier
做用於線程;CountDownLatch
是在調用了 countDown
方法以後把數字倒數減 1,而 CyclicBarrier
是在某線程開始等待後把計數減 1。CountDownLatch
在倒數到 0 而且觸發門閂打開後,就不能再次使用了,除非新建一個新的實例;而 CyclicBarrier
能夠重複使用。CyclicBarrier
還能夠隨時調用 reset 方法進行重置,若是重置時有線程已經調用了 await 方法並開始等待,那麼這些線程則會拋出 BrokenBarrierException
異常。CyclicBarrier
有執行動做 barrierAction
,而 CountDownLatch
沒這個功能。CountDownLatch
和CyclicBarrier
都用於實現多線程之間的相互等待,但兩者的關注點不一樣。CountDownLatch
主要用於主線程等待其餘子線程任務均執行完畢後再執行接下來的業務邏輯單元,而CyclicBarrier
主要用於一組線程互相等待你們都達到某個狀態後,再同時執行接下來的業務邏輯單元。此外,CountDownLatch
是不能夠重用的,而CyclicBarrier
是能夠重用的。Semaphore
和Java
中的鎖功能相似,主要用於控制資源的併發訪問。ReentrantLock
支持公平鎖和非公平鎖兩種方式。公平鎖指鎖的分配和競爭機制是公平的,即遵循先到先得原則。非公平鎖指JVM
遵循隨機、就近原則分配鎖的機制。ReentrantLock
經過在構造函數ReentrantLock(boolean fair)
中傳遞不一樣的參數來定義不一樣類型的鎖,默認的實現是非公平鎖。這是由於,非公平鎖雖然放棄了鎖的公平性,可是執行效率明顯高於公平鎖。若是系統沒有特殊的要求,通常狀況下建議使用非公平鎖。
synchronized
能夠給類,方法,代碼塊加鎖,而 lock
只能給代碼塊加鎖。synchronized
不須要手動獲取鎖和釋放鎖,使用簡單,發生異常會自動釋放鎖,不會形成死鎖,而 lock
須要手動本身加鎖和釋放鎖,若是使用不當沒有 unLock
去釋放鎖,就會形成死鎖。lock
能夠知道有沒有成功獲取鎖,而 synchronized
沒法辦到。synchronized
和 Lock
都是用來保護資源線程安全的。synchronized
和 ReentrantLock
都擁有可重入的特色。不一樣點:
lock
須要配合finally
)ReentrantLock
可響應中斷、可輪迴,爲處理鎖提供了更多的靈活性ReentrantLock
經過Condition
能夠綁定多個條件synchronized
鎖不夠靈活synchronized
是同步阻塞,採用的是悲觀併發策略;Lock
是同步非阻塞,採用的是樂觀併發策略。使用
Lock
也不使用 synchronized
。synchronized
關鍵字適合你的程序,這樣能夠減小編寫代碼的數量,減小出錯的機率Lock
的特殊功能,好比嘗試獲取鎖、可中斷、超時功能等,才使用 Lock
。void lock()
:獲取鎖,調用該方法當前線程將會獲取鎖,當鎖得到後,從該方法返回void lockInterruptibly() throws InterruptedException
:可中斷地獲取鎖,和lock
方法地不一樣之處在於該方法會響應中斷,即在鎖的獲取中能夠中斷當前線程boolean tryLock()
: 嘗試非阻塞地獲取鎖,調用該方法後馬上返回,若是可以獲取則返回 true 不然 返回falseboolean tryLock(long time, TimeUnit unit)
:超時地獲取鎖,當前線程在如下 3 種狀況下會返回:void unlock()
: 釋放鎖Condition newCondition()
:獲取鎖等待通知組件,該組件和當前的鎖綁定,當前線程只有得到了鎖,才能調用該組件的 wait()
方法,而調用後,當前線程將釋放鎖。tryLock
、lock
和lockInterruptibly
的區別以下。
tryLock
如有可用鎖,則獲取該鎖並返回true,不然返回false,不會有延遲或等待;tryLock(long timeout, TimeUnit unit)
能夠增長時間限制,若是超過了指定的時間還沒得到鎖,則返回 false。lock
如有可用鎖,則獲取該鎖並返回true,不然會一直等待直到獲取可用鎖。lockInterruptibly
會拋出異常,lock
不會。突擊併發編程JUC系列-ReentrantLock
要麼是一個或多個線程同時有讀鎖,要麼是一個線程有寫鎖,可是二者不會同時出現。也能夠總結爲:讀讀共享、其餘都互斥(寫寫互斥、讀寫互斥、寫讀互斥)
ReentrantLock
適用於通常場合,ReadWriteLock
適用於讀多寫少的狀況,合理使用能夠進一步提升併發效率。
突擊併發編程JUC系列-ReentrantReadWriteLock
ReentrantReadWriteLock
的實現選擇了「不容許插隊」的策略,這就大大減少了發生「飢餓」的機率。
插隊策略
升降級策略:只能從寫鎖降級爲讀鎖,不能從讀鎖升級爲寫鎖。
tryLock(long timeout,TimeUnit unit)
的方法(ReentrantLock 、ReenttranReadWriteLock
)設置超時時間,超時能夠退出防止死鎖。 java.util.concurrent
併發類代替手寫鎖。Condition
類的 awiat
方法和 Object
類的 wait
方法等效Condition
類的 signal
方法和 Object
類的 notify
方法等效Condition
類的 signalAll
方法和 Object
類的 notifyAll
方法等效ReentrantLock
類能夠喚醒指定條件的線程,而 object 的喚醒是隨機的HashTable
使用一把鎖(鎖住整個鏈表結構)處理併發問題,多個線程競爭一把鎖,容易阻塞;ConcurrentHashMap
JDK 1.7
中使用分段鎖(ReentrantLock + Segment + HashEntry
),至關於把一個 HashMap
分紅多個段,每段分配一把鎖,這樣支持多線程訪問。鎖粒度:基於 Segment,包含多個 HashEntry
。JDK 1.8
中使用 CAS + synchronized + Node + 紅黑樹
。鎖粒度:Node(首結點)(實現 Map.Entry
)。鎖粒度下降了。
JDK 1.7
中的ConcurrentHashMap
內部進行了 Segment
分段,Segment
繼承了 ReentrantLock
,能夠理解爲一把鎖,各個 Segment
之間都是相互獨立上鎖的,互不影響。
相比於以前的 Hashtable
每次操做都須要把整個對象鎖住而言,大大提升了併發效率。由於它的鎖與鎖之間是獨立的,而不是整個對象只有一把鎖。
每一個 Segment 的底層數據結構與 HashMap
相似,仍然是數組和鏈表組成的拉鍊法結構。默認有 0~15 共 16 個 Segment
,因此最多能夠同時支持 16 個線程併發操做(操做分別分佈在不一樣的 Segment
上)。16 這個默認值能夠在初始化的時候設置爲其餘值,可是一旦確認初始化之後,是不能夠擴容的。
圖中的節點有三種類型:
HashMap
很是相似的拉鍊法結構,在每個槽中會首先填入第一個節點,可是後續若是計算出相同的 Hash 值,就用鏈表的形式日後進行延伸。ConcurrentHashMap
中所沒有的結構,在此以前咱們可能也不多接觸這樣的數據結構鏈表長度大於某一個閾值(默認爲 8),知足容量從鏈表的形式轉化爲紅黑樹的形式。
紅黑樹是每一個節點都帶有顏色屬性的二叉查找樹,顏色爲紅色或黑色,紅黑樹的本質是對二叉查找樹 BST 的一種平衡策略,咱們能夠理解爲是一種平衡二叉查找樹,查找效率高,會自動平衡,防止極端不平衡從而影響查找效率的狀況發生,紅黑樹每一個節點要麼是紅色,要麼是黑色,但根節點永遠是黑色的。
Node[]
數組是否初始化,沒有則進行初始化操做hash
定位數組的索引座標,是否有 Node
節點,若是沒有則使用 CAS
進行添加(鏈表的頭節點),添加失敗則進入下次循環。synchronized
鎖住 f 元素(鏈表/紅黑二叉樹的頭元素)Node
(鏈表結構)則執行鏈表的添加操做TreeNode
(樹形結構)則執行樹添加操做。突擊併發編程JUC系列-併發容器ConcurrentHashMap
阻塞隊列(BlockingQueue
)是一個支持兩個附加操做的隊列。這兩個附加的操做支持阻塞的插入和移除方法。
阻塞隊列經常使用於生產者和消費者的場景,生產者是向隊列裏添加元素的線程,消費者是從隊列裏取元素的線程。阻塞隊列就是生產者用來存放元素、消費者用來獲取元素的容器。
ArrayBlockingQueue
:一個由數組結構組成的有界阻塞隊列。LinkedBlockingQueue
:一個由鏈表結構組成的有界阻塞隊列。PriorityBlockingQueue
:一個支持優先級排序的無界阻塞隊列。DelayQueue
:一個使用優先級隊列實現的無界阻塞隊列。SynchronousQueue
:一個不存儲元素的阻塞隊列。LinkedTransferQueue
:一個由鏈表結構組成的無界阻塞隊列。LinkedBlockingDeque
:一個由鏈表結構組成的雙向阻塞隊列。突擊併發編程JUC系列-阻塞隊列 BlockingQueue
Java 中的線程池是運用場景最多的併發框架,幾乎全部須要異步或併發執行任務的程序均可以使用線程池。
當提交一個新任務到線程池時,線程池的處理流程以下:
ThreadPoolExecutor
執行execute()
方法的示意圖 以下:
ThreadPoolExecutor
執行execute
方法分下面 4 種狀況:
corePoolSize
,則建立新線程來執行任務(注意,執行這一步驟須要獲取全局鎖)。corePoolSize
,則將任務加入BlockingQueue
。BlockingQueue
(隊列已滿),則建立新的線程來處理任務(注意,執行這一步驟須要獲取全局鎖)。maximumPoolSize
,任務將被拒絕,並調用RejectedExecutionHandler.rejectedExecution()
方法。ThreadPoolExecutor
採起上述步驟的整體設計思路,是爲了在執行execute()方法時,儘量地避免獲取全局鎖(那將會是一個嚴重的可伸縮瓶頸)。在ThreadPoolExecutor
完成預熱以後(當前運行的線程數大於等於corePoolSize
),幾乎全部的execute()
方法調用都是執行步驟 2,而步驟2不須要獲取全局鎖。
NEW(初始)
,新建狀態,線程被建立出來,但還沒有啓動時的線程狀態;RUNNABLE(就緒狀態)
,表示能夠運行的線程狀態,它可能正在運行,或者是在排隊等待操做系統給它分配 CPU 資源;BLOCKED(阻塞)
,阻塞等待鎖的線程狀態,表示處於阻塞狀態的線程正在等待監視器鎖,好比等待執行 synchronized
代碼塊或者使用 synchronized
標記的方法;WAITING(等待)
,等待狀態,一個處於等待狀態的線程正在等待另外一個線程執行某個特定的動做,好比,一個線程調用了 Object.wait()
方法,那它就在等待另外一個線程調用 Object.notify()
或 Object.notifyAll()
方法;TIMED_WAITING(超時等待)
,計時等待狀態,和等待狀態(WAITING)
相似,它只是多了超時時間,好比調用了有超時時間設置的方法 Object.wait(long timeout)
和 Thread.join(long timeout)
等這些方法時,它纔會進入此狀態;TERMINATED
,終止狀態,表示線程已經執行完成。
running
:這是最正常的狀態,接受新的任務,處理等待隊列中的任務。shutdown
:不接受新的任務提交,可是會繼續處理等待隊列中的任務。stop
:不接受新的任務提交,再也不處理等待隊列中的任務,中斷正在執行任務的線程。tidying
:全部的任務都銷燬了,workcount
爲 0,線程池的狀態再轉換 tidying 狀態時,會執行鉤子方法 terminated()
。terminated
: terminated()
方法結束後,線程池的狀態就會變成這個。
execute()
: 只能執行 Runable
類型的任務。submit()
能夠執行 Runable
和 Callable
類型的任務。 Callable
類型的任務能夠獲取執行的返回值,而 Runnable
執行無返回值。
newSingleThreadExecutor()
: 他的特色是在於線程數目被限制位1:操做一個無界的工做隊列,因此它保證了全部的任務的都是順序執行,最多會有一個任務處於活動狀態,而且不容許使用者改動線程池實例,所以能夠避免其改變線程數目。newCachedThreadPool()
:它是一種用來處理大量短期工做任務的線程,具備幾個鮮明的特色,它會試圖緩存線程並重用,當無緩存線程可用時,就會建立新的工做線程,若是線程閒置的時間超過 60 秒,則被終止並移除緩存;長時間閒置時,這種線程池不會消耗什麼資源,其內部使用 synchronousQueue
做爲工做隊列。newFixedThreadPool(int nThreads)
:重用指定數目 nThreads 的線程,其背後使用的無界的工做隊列,任什麼時候候最後有 nThreads 個工做線程活動的,這意味着 若是任務數量超過了活動隊列數目,將在工做隊列中等待空閒線程出現,若是有工做線程退出,將會有新的工做線程被建立,以補足指定的數目 nThreads。newSingleThreadScheduledExecutor()
: 建立單線程池,返回ScheduleExecutorService
能夠進行定時或週期性的工做強度。newScheduleThreadPool(int corePoolSize)
: 和 newSingleThreadSceduleExecutor()
相似,建立的ScheduledExecutorService
能夠進行定時或週期的工做調度,區別在於單一工做線程仍是工做線程。newWorkStrealingPool(int parallelism)
:這是一個常常被人忽略的線程池,Java 8 才加入這個建立方法,其內部會構建ForkJoinPool
利用 work-strealing
算法 並行的處理任務,不保證處理順序。ThreadPollExecutor
: 是最原始的線程池建立,上面 1-3 建立方式 都是對ThreadPoolExecutor
的封裝。上面 7 種建立方式中,前 6 種 經過Executors
工廠方法建立,ThreadPoolExecutor
手動建立。
下面介紹下 ThreadPoolExecutor
接收 7 個參數的構造方法
/** * 用給定的初始參數建立一個新的ThreadPoolExecutor。 */ public ThreadPoolExecutor(int corePoolSize,//線程池的核心線程數量 int maximumPoolSize,//線程池的最大線程數 long keepAliveTime,//當線程數大於核心線程數時,多餘的空閒線程存活的最長時間 TimeUnit unit,//時間單位 BlockingQueue<Runnable> workQueue,//任務隊列 ThreadFactory threadFactory,//線程工廠 RejectedExecutionHandler handler//拒絕策略 )
corePoolSize
: 核心線程數線程數定義了最小能夠同時運行的線程數量。maximumPoolSize
: 當隊列中存放的任務達到隊列容量的時候,當前能夠同時運行的線程數量變爲最大線程數。workQueue
: 當新任務來的時候會先判斷當前運行的線程數量是否達到核心線程數,若是達到的話,信任就會被存放在隊列中。keepAliveTime
:線程活動保持時間,當線程池中的線程數量大於 corePoolSize
的時候,若是這時沒有新的任務提交,核心線程外的線程不會當即銷燬,而是會等待,直到等待的時間超過了 keepAliveTime
纔會被回收銷燬;unit
: keepAliveTime
參數的時間單位。threadFactory
: 任務隊列,用於保存等待執行的任務的阻塞隊列。能夠選擇如下幾個阻塞隊列。
ArrayBlockingQueue
:是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO
(先進先出)原則對元素進行排序。LinkedBlockingQueue
:一個基於鏈表結構的阻塞隊列,此隊列按FIFO
排序元素,吞吐量一般要高於ArrayBlockingQueue
。靜態工廠方法Executors.newFixedThreadPool()
使用了這個隊列。SynchronousQueue
:一個不存儲元素的阻塞隊列。每一個插入操做必須等到另外一個線程調用移除操做,不然插入操做一直處於阻塞狀態,吞吐量一般要高於Linked-BlockingQueue
,靜態工廠方法Executors.newCachedThreadPool
使用了這個隊列。PriorityBlockingQueue
:一個具備優先級的無限阻塞隊列。handler
:飽和策略(又稱拒絕策略)。當隊列和線程池都滿了,說明線程池處於飽和狀態,那麼必須採起一種策略處理提交的新任務。這個策略默認狀況下是AbortPolicy
,表示沒法處理新任務時拋出異常。在JDK 1.5
中 Java 線程池框架提供瞭如下4種策略。
AbortPolicy
:直接拋出異常。CallerRunsPolicy
:只用調用者所在線程來運行任務。DiscardOldestPolicy
:丟棄隊列裏最近的一個任務,並執行當前任務。DiscardPolicy
:不處理,丟棄掉歡迎關注公衆號 山間木匠 , 我是小春哥,從事 Java 後端開發,會一點前端、經過持續輸出系列技術文章以文會友,若是本文能爲您提供幫助,歡迎你們關注、 點贊、分享支持,_咱們下期再見!_