突擊并發(fā)編程JUC系列-萬字長文解密 JUC 面試題

突擊并發(fā)編程JUC系列演示代碼地址:
https://github.com/mtcarpenter/JavaTutorial

什么是 CAS 嗎?

CAS(Compare And Swap)指比較并交換。CAS算法CAS(V, E, N)包含 3 個參數(shù),V 表示要更新的變量,E 表示預(yù)期的值,N 表示新值。在且僅在 V 值等于 E值時,才會將 V 值設(shè)為 N,如果 V 值和 E 值不同,則說明已經(jīng)有其他線程做了更新,當(dāng)前線程什么都不做。最后,CAS 返回當(dāng)前 V 的真實值。Concurrent包下所有類底層都是依靠CAS操作來實現(xiàn),而sun.misc.Unsafe為我們提供了一系列的CAS操作。

CAS 有什么缺點?

  • ABA問題
  • 自旋問題
  • 范圍不能靈活控制

對 CAS 中的 ABA 產(chǎn)生有解決方案嗎?

什么是 ABA 問題呢?多線程環(huán)境下。線程 1 從內(nèi)存的V位置取出 A ,線程 2 也從內(nèi)存中取出 A,并將 V 位置的數(shù)據(jù)首先修改為 B,接著又將 V 位置的數(shù)據(jù)修改為 A,線程 1 在進行CAS操作時會發(fā)現(xiàn)在內(nèi)存中仍然是 A,線程 1 操作成功。盡管從線程 1 的角度來說,CAS操作是成功的,但在該過程中其實 V 位置的數(shù)據(jù)發(fā)生了變化,線程 1 沒有感知到罷了,這在某些應(yīng)用場景下可能出現(xiàn)過程數(shù)據(jù)不一致的問題。

可以版本號(version)來解決 ABA 問題的,在 atomic 包中提供了AtomicStampedReference 這個類,它是專門用來解決 ABA 問題的。

直達(dá)鏈接: AtomicStampedReference ABA 案例鏈接

CAS 自旋導(dǎo)致的問題?

由于單次 CAS 不一定能執(zhí)行成功,所以 CAS往往是配合著循環(huán)來實現(xiàn)的,有的時候甚至是死循環(huán),不停地進行重試,直到線程競爭不激烈的時候,才能修改成功。

CPU 資源也是一直在被消耗的,這會對性能產(chǎn)生很大的影響。所以這就要求我們,要根據(jù)實際情況來選擇是否使用 CAS,在高并發(fā)的場景下,通常 CAS 的效率是不高的。

CAS 范圍不能靈活控制

不能靈活控制線程安全的范圍。只能針對某一個,而不是多個共享變量的,不能針對多個共享變量同時進行 CAS操作,因為這多個變量之間是獨立的,簡單的把原子操作組合到一起,并不具備原子性。

什么是 AQS 嗎?

AbstractQueuedSynchronizer抽象同步隊列簡稱AQS,它是實現(xiàn)同步器的基礎(chǔ)組件,并發(fā)包中鎖的底層就是使用AQS實現(xiàn)的。AQS定義了一套多線程訪問共享資源的同步框架,許多同步類的實現(xiàn)都依賴于它,例如常用的Synchronized、ReentrantLockReentrantReadWriteLockSemaphore、CountDownLatch等。該框架下的鎖會先嘗試以CAS樂觀鎖去獲取鎖,如果獲取不到,則會轉(zhuǎn)為悲觀鎖(如RetreenLock)。

了解 AQS 共享資源的方式嗎?

  • 獨占式:只有一個線程能執(zhí)行,具體的Java實現(xiàn)有ReentrantLock。
  • 共享式:多個線程可同時執(zhí)行,具體的Java實現(xiàn)有SemaphoreCountDownLatch

Atomic 原子更新

JavaJDK1.5 開始提供了 java.util.concurrent.atomic 包,方便程序員在多線程環(huán) 境下,無鎖的進行原子操作。在 Atomic 包里一共有 12 個類,四種原子更新方式,分別是原子更新基本類型,原子更新數(shù)組,原子更新引用原子更新字段。在 JDK 1.8 之后又新增幾個原子類。如下如:

針對思維導(dǎo)圖知識點在前面的章節(jié)都進行了理論+實踐的講解,到達(dá)地址如下:

突擊并發(fā)編程JUC系列-原子更新AtomicLong

突擊并發(fā)編程JUC系列-數(shù)組類型AtomicLongArray

突擊并發(fā)編程JUC系列-原子更新字段類AtomicStampedReference

突擊并發(fā)編程JUC系列-JDK1.8 擴展類型 LongAdder

列舉幾個AtomicLong 的常用方法

  • long getAndIncrement() :以原子方式將當(dāng)前值加1,注意,返回的是舊值。(i++)
  • long incrementAndGet() :以原子方式將當(dāng)前值加1,注意,返回的是新值。(++i)
  • long getAndDecrement() :以原子方式將當(dāng)前值減 1,注意,返回的是舊值 。(i--)
  • long decrementAndGet() :以原子方式將當(dāng)前值減 1,注意,返回的是新值 。(--i)
  • long addAndGet(int delta) :以原子方式將輸入的數(shù)值與實例中的值(AtomicLong里的value)相加,并返回結(jié)果

說說 AtomicInteger 和 synchronized 的異同點?

相同點

  • 都是線程安全

不同點

  • 1、背后原理
    synchronized 背后的 monitor 鎖。在執(zhí)行同步代碼之前,需要首先獲取到 monitor 鎖,執(zhí)行完畢后,再釋放鎖。原子類,線程安全的原理是利用了 CAS 操作。
  • 2、使用范圍
    原子類使用范圍是比較局限的,一個原子類僅僅是一個對象,不夠靈活。而 synchronized 的使用范圍要廣泛得多。比如說 synchronized 既可以修飾一個方法,又可以修飾一段代碼,相當(dāng)于可以根據(jù)我們的需要,非常靈活地去控制它的應(yīng)用范圍
  • 3、粒度
    原子變量的粒度是比較小的,它可以把競爭范圍縮小到變量級別。通常情況下,synchronized鎖的粒度都要大于原子變量的粒度。
  • 4、性能
    synchronized是一種典型的悲觀鎖,而原子類恰恰相反,它利用的是樂觀鎖。

原子類和 volatile 有什么異同?

  • volatile 可見性問題
  • 解決原子性問題

AtomicLong 可否被 LongAdder 替代?

有了更高效的 LongAdder,那AtomicLong 可否不使用了呢?是否凡是用到 AtomicLong的地方,都可以用LongAdder替換掉呢?答案是不是的,這需要區(qū)分場景。

LongAdder 只提供了 add、increment 等簡單的方法,適合的是統(tǒng)計求和計數(shù)的場景,場景比較單一,而 AtomicLong 還具有 compareAndSet 等高級方法,可以應(yīng)對除了加減之外的更復(fù)雜的需要CAS 的場景。

結(jié)論:如果我們的場景僅僅是需要用到加和減操作的話,那么可以直接使用更高效的 LongAdder,但如果我們需要利用 CAS 比如compareAndSet 等操作的話,就需要使用 AtomicLong 來完成。

直達(dá)鏈接:突擊并發(fā)編程JUC系列-JDK1.8 擴展類型 LongAdder

并發(fā)工具

CountDownLatch

CountDownLatch基于線程計數(shù)器來實現(xiàn)并發(fā)訪問控制,主要用于主線程等待其他子線程都執(zhí)行完畢后執(zhí)行相關(guān)操作。其使用過程為:在主線程中定義CountDownLatch,并將線程計數(shù)器的初始值設(shè)置為子線程的個數(shù),多個子線程并發(fā)執(zhí)行,每個子線程在執(zhí)行完畢后都會調(diào)用countDown函數(shù)將計數(shù)器的值減1,直到線程計數(shù)器為0,表示所有的子線程任務(wù)都已執(zhí)行完畢,此時在CountDownLatch上等待的主線程將被喚醒并繼續(xù)執(zhí)行。

突擊并發(fā)編程JUC系列-并發(fā)工具 CountDownLatch

CyclicBarrier

CyclicBarrier(循環(huán)屏障)是一個同步工具,可以實現(xiàn)讓一組線程等待至某個狀態(tài)之后再全部同時執(zhí)行。在所有等待線程都被釋放之后,CyclicBarrier可以被重用。CyclicBarrier的運行狀態(tài)叫作Barrier狀態(tài),在調(diào)用await方法后,線程就處于Barrier狀態(tài)。

CyclicBarrier中最重要的方法是await方法,它有兩種實現(xiàn)。

  • public int await():掛起當(dāng)前線程直到所有線程都為Barrier狀態(tài)再同時執(zhí)行后續(xù)的任務(wù)。
  • public int await(long timeout, TimeUnit unit):設(shè)置一個超時時間,在超時時間過后,如果還有線程未達(dá)到Barrier狀態(tài),則不再等待,讓達(dá)到Barrier狀態(tài)的線程繼續(xù)執(zhí)行后續(xù)的任務(wù)。

突擊并發(fā)編程JUC系列-并發(fā)工具 CyclicBarrier

Semaphore

Semaphore指信號量,用于控制同時訪問某些資源的線程個數(shù),具體做法為通過調(diào)用acquire()獲取一個許可,如果沒有許可,則等待,在許可使用完畢后通過release()釋放該許可,以便其他線程使用。

突擊并發(fā)編程JUC系列-并發(fā)工具 Semaphore

CyclicBarrier 和 CountdownLatch 有什么異同?

相同點:都能阻塞一個或一組線程,直到某個預(yù)設(shè)的條件達(dá)成發(fā)生,再統(tǒng)一出發(fā)。
但是它們也有很多不同點,具體如下。

  • 作用對象不同:CyclicBarrier 要等固定數(shù)量的線程都到達(dá)了柵欄位置才能繼續(xù)執(zhí)行,而 CountDownLatch 只需等待數(shù)字倒數(shù)到 0,也就是說 CountDownLatch 作用于事件,但 CyclicBarrier 作用于線程;CountDownLatch 是在調(diào)用了 countDown 方法之后把數(shù)字倒數(shù)減 1,而 CyclicBarrier 是在某線程開始等待后把計數(shù)減 1。
  • 可重用性不同:CountDownLatch 在倒數(shù)到 0 并且觸發(fā)門閂打開后,就不能再次使用了,除非新建一個新的實例;而 CyclicBarrier 可以重復(fù)使用。CyclicBarrier還可以隨時調(diào)用 reset 方法進行重置,如果重置時有線程已經(jīng)調(diào)用了 await 方法并開始等待,那么這些線程則會拋出 BrokenBarrierException異常。
  • 執(zhí)行動作不同:CyclicBarrier 有執(zhí)行動作 barrierAction,而 CountDownLatch沒這個功能。

CountDownLatch、CyclicBarrier、Semaphore的區(qū)別如下。

  • CountDownLatchCyclicBarrier都用于實現(xiàn)多線程之間的相互等待,但二者的關(guān)注點不同。CountDownLatch主要用于主線程等待其他子線程任務(wù)均執(zhí)行完畢后再執(zhí)行接下來的業(yè)務(wù)邏輯單元,而CyclicBarrier主要用于一組線程互相等待大家都達(dá)到某個狀態(tài)后,再同時執(zhí)行接下來的業(yè)務(wù)邏輯單元。此外,CountDownLatch是不可以重用的,而CyclicBarrier是可以重用的。
  • SemaphoreJava中的鎖功能類似,主要用于控制資源的并發(fā)訪問。

locks

公平鎖與非公平鎖

ReentrantLock支持公平鎖和非公平鎖兩種方式。公平鎖指鎖的分配和競爭機制是公平的,即遵循先到先得原則。非公平鎖指JVM遵循隨機、就近原則分配鎖的機制。ReentrantLock通過在構(gòu)造函數(shù)ReentrantLock(boolean fair)中傳遞不同的參數(shù)來定義不同類型的鎖,默認(rèn)的實現(xiàn)是非公平鎖。這是因為,非公平鎖雖然放棄了鎖的公平性,但是執(zhí)行效率明顯高于公平鎖。如果系統(tǒng)沒有特殊的要求,一般情況下建議使用非公平鎖。

synchronized 和 lock 有什么區(qū)別?

  • synchronized 可以給類,方法,代碼塊加鎖,而lock 只能給代碼塊加鎖。
  • synchronized 不需要手動獲取鎖和釋放鎖,使用簡單,發(fā)生異常會自動釋放鎖,不會造成死鎖,而 lock 需要手動自己加鎖和釋放鎖,如果使用不當(dāng)沒有 unLock 去釋放鎖,就會造成死鎖。
  • 通過 lock 可以知道有沒有成功獲取鎖,而 synchronized 無法辦到。

synchronized 和 Lock 如何選擇?

  • synchronizedLock 都是用來保護資源線程安全的。
  • 都保證了可見性和互斥性。
  • synchronizedReentrantLock都擁有可重入的特點。

不同點:

  • 用法(lock 需要配合finally
  • ReentrantLock可響應(yīng)中斷、可輪回,為處理鎖提供了更多的靈活性
  • ReentrantLock通過Condition可以綁定多個條件
  • 加解鎖順序()
  • synchronized 鎖不夠靈活
  • 是否可以設(shè)置公平/非公平
  • 二者的底層實現(xiàn)不一樣:synchronized是同步阻塞,采用的是悲觀并發(fā)策略;Lock是同步非阻塞,采用的是樂觀并發(fā)策略。

使用

  • 如果能不用最好既不使用 Lock 也不使用 synchronized。
  • 如果 synchronized關(guān)鍵字適合你的程序,這樣可以減少編寫代碼的數(shù)量,減少出錯的概率
  • 如果特別需要 Lock 的特殊功能,比如嘗試獲取鎖、可中斷、超時功能等,才使用 Lock

Lock接口的主要方法

  • void lock():獲取鎖,調(diào)用該方法當(dāng)前線程將會獲取鎖,當(dāng)鎖獲得后,從該方法返回
  • void lockInterruptibly() throws InterruptedException:可中斷地獲取鎖,和lock方法地不同之處在于該方法會響應(yīng)中斷,即在鎖的獲取中可以中斷當(dāng)前線程
  • boolean tryLock(): 嘗試非阻塞地獲取鎖,調(diào)用該方法后立刻返回,如果能夠獲取則返回 true 否則 返回false
  • boolean tryLock(long time, TimeUnit unit):超時地獲取鎖,當(dāng)前線程在以下 3 種情況下會返回:
    • 當(dāng)前線程在超時時間內(nèi)獲得了鎖
    • 當(dāng)前線程在超時時間被中斷
    • 超時時間結(jié)束后,返回 false
  • void unlock(): 釋放鎖
  • Condition newCondition():獲取鎖等待通知組件,該組件和當(dāng)前的鎖綁定,當(dāng)前線程只有獲得了鎖,才能調(diào)用該組件的 wait() 方法,而調(diào)用后,當(dāng)前線程將釋放鎖。

tryLock、lock和lockInterruptibly的區(qū)別

tryLock、locklockInterruptibly的區(qū)別如下。

  • tryLock若有可用鎖,則獲取該鎖并返回true,否則返回false,不會有延遲或等待;tryLock(long timeout, TimeUnit unit)可以增加時間限制,如果超過了指定的時間還沒獲得鎖,則返回 false。
  • lock若有可用鎖,則獲取該鎖并返回true,否則會一直等待直到獲取可用鎖。
  • 在鎖中斷時lockInterruptibly會拋出異常,lock不會。

突擊并發(fā)編程JUC系列-ReentrantLock

ReentrantReadWriteLock 讀寫鎖的獲取規(guī)則

要么是一個或多個線程同時有讀鎖,要么是一個線程有寫鎖,但是兩者不會同時出現(xiàn)。也可以總結(jié)為:讀讀共享、其他都互斥(寫寫互斥、讀寫互斥、寫讀互斥)

ReentrantLock 適用于一般場合,ReadWriteLock 適用于讀多寫少的情況,合理使用可以進一步提高并發(fā)效率。

突擊并發(fā)編程JUC系列-ReentrantReadWriteLock

讀鎖應(yīng)該插隊嗎?什么是讀寫鎖的升降級?

ReentrantReadWriteLock 的實現(xiàn)選擇了“不允許插隊”的策略,這就大大減小了發(fā)生“饑餓”的概率。

插隊策略

  • 公平策略下,只要隊列里有線程已經(jīng)在排隊,就不允許插隊。
  • 非公平策略下:
    • 如果允許讀鎖插隊,那么由于讀鎖可以同時被多個線程持有,所以可能造成源源不斷的后面的線程一直插隊成功,導(dǎo)致讀鎖一直不能完全釋放,從而導(dǎo)致寫鎖一直等待,為了防止“饑餓”,在等待隊列的頭結(jié)點是嘗試獲取寫鎖的線程的時候,不允許讀鎖插隊。
    • 寫鎖可以隨時插隊,因為寫鎖并不容易插隊成功,寫鎖只有在當(dāng)前沒有任何其他線程持有讀鎖和寫鎖的時候,才能插隊成功,同時寫鎖一旦插隊失敗就會進入等待隊列,所以很難造成“饑餓”的情況,允許寫鎖插隊是為了提高效率。

升降級策略:只能從寫鎖降級為讀鎖,不能從讀鎖升級為寫鎖。

怎么防止死鎖?

  • 盡量使用 tryLock(long timeout,TimeUnit unit) 的方法(ReentrantLock 、ReenttranReadWriteLock)設(shè)置超時時間,超時可以退出防止死鎖。
  • 盡量使用java.util.concurrent并發(fā)類代替手寫鎖。
  • 盡量降低鎖的使用粒度,盡量不要幾個功能用同一把鎖。
  • 盡量減少同步的代碼塊。

Condition 類和 Object 類鎖方法區(qū)別區(qū)別

  • Condition類的 awiat 方法和 Object 類的wait方法等效
  • Condition 類的 signal方法和 Object 類的notify 方法等效
  • Condition 類的 signalAll 方法和 Object 類的 notifyAll 方法等效
  • ReentrantLock 類可以喚醒指定條件的線程,而 object 的喚醒是隨機的

并發(fā)容器

為什么 ConcurrentHashMap 比 HashTable 效率要高?

  • HashTable 使用一把鎖(鎖住整個鏈表結(jié)構(gòu))處理并發(fā)問題,多個線程競爭一把鎖,容易阻塞;
  • ConcurrentHashMap
    • JDK 1.7 中使用分段鎖(ReentrantLock + Segment + HashEntry),相當(dāng)于把一個HashMap 分成多個段,每段分配一把鎖,這樣支持多線程訪問。鎖粒度:基于 Segment,包含多個 HashEntry。
    • JDK 1.8中使用 CAS + synchronized + Node + 紅黑樹。鎖粒度:Node(首結(jié)點)(實現(xiàn) Map.Entry)。鎖粒度降低了。

ConcurrentHashMap JDK 1.7/JDK 1.8

JDK 1.7 結(jié)構(gòu)

JDK 1.7 中的ConcurrentHashMap 內(nèi)部進行了 Segment分段,Segment 繼承了 ReentrantLock,可以理解為一把鎖,各個 Segment 之間都是相互獨立上鎖的,互不影響。
相比于之前的 Hashtable 每次操作都需要把整個對象鎖住而言,大大提高了并發(fā)效率。因為它的鎖與鎖之間是獨立的,而不是整個對象只有一把鎖。
每個 Segment 的底層數(shù)據(jù)結(jié)構(gòu)與 HashMap 類似,仍然是數(shù)組和鏈表組成的拉鏈法結(jié)構(gòu)。默認(rèn)有 0~15 共 16 個 Segment,所以最多可以同時支持 16 個線程并發(fā)操作(操作分別分布在不同的 Segment 上)。16 這個默認(rèn)值可以在初始化的時候設(shè)置為其他值,但是一旦確認(rèn)初始化以后,是不可以擴容的。

JDK 1.8 結(jié)構(gòu)

圖中的節(jié)點有三種類型:

  • 第一種是最簡單的,空著的位置代表當(dāng)前還沒有元素來填充。
  • 第二種就是和 HashMap 非常類似的拉鏈法結(jié)構(gòu),在每一個槽中會首先填入第一個節(jié)點,但是后續(xù)如果計算出相同的 Hash 值,就用鏈表的形式往后進行延伸。
  • 第三種結(jié)構(gòu)就是紅黑樹結(jié)構(gòu),這是 Java 7 的 ConcurrentHashMap 中所沒有的結(jié)構(gòu),在此之前我們可能也很少接觸這樣的數(shù)據(jù)結(jié)構(gòu)

鏈表長度大于某一個閾值(默認(rèn)為 8),滿足容量從鏈表的形式轉(zhuǎn)化為紅黑樹的形式。
紅黑樹是每個節(jié)點都帶有顏色屬性的二叉查找樹,顏色為紅色或黑色,紅黑樹的本質(zhì)是對二叉查找樹 BST 的一種平衡策略,我們可以理解為是一種平衡二叉查找樹,查找效率高,會自動平衡,防止極端不平衡從而影響查找效率的情況發(fā)生,紅黑樹每個節(jié)點要么是紅色,要么是黑色,但根節(jié)點永遠(yuǎn)是黑色的。

ConcurrentHashMap 中 get 的過程

  • 計算 Hash 值,并由此值找到對應(yīng)的槽點;
  • 如果數(shù)組是空的或者該位置為 null,那么直接返回 null 就可以了;
  • 如果該位置處的節(jié)點剛好就是我們需要的,直接返回該節(jié)點的值;
  • 如果該位置節(jié)點是紅黑樹或者正在擴容,就用 find 方法繼續(xù)查找;
  • 否則那就是鏈表,就進行遍歷鏈表查找

ConcurrentHashMap 中 put 的過程

  • 判斷 Node[] 數(shù)組是否初始化,沒有則進行初始化操作
  • 通過 hash 定位數(shù)組的索引坐標(biāo),是否有 Node 節(jié)點,如果沒有則使用 CAS 進行添加(鏈表的頭節(jié)點),添加失敗則進入下次循環(huán)。
  • 檢查到內(nèi)部正在擴容,就幫助它一塊擴容。
  • 如果 f != null ,則使用 synchronized 鎖住 f 元素(鏈表/紅黑二叉樹的頭元素)
    • 如果是 Node (鏈表結(jié)構(gòu))則執(zhí)行鏈表的添加操作
    • 如果是 TreeNode (樹形結(jié)構(gòu))則執(zhí)行樹添加操作。
  • 判斷鏈表長度已經(jīng)達(dá)到臨界值 8 ,當(dāng)然這個 8 是默認(rèn)值,大家也可以去做調(diào)整,當(dāng)節(jié)點數(shù)超過這個值就需要把鏈表轉(zhuǎn)換為樹結(jié)構(gòu)。

突擊并發(fā)編程JUC系列-并發(fā)容器ConcurrentHashMap

什么是阻塞隊列?

阻塞隊列(BlockingQueue)是一個支持兩個附加操作的隊列。這兩個附加的操作支持阻塞的插入和移除方法。

  • 支持阻塞的插入方法:意思是當(dāng)隊列滿時,隊列會阻塞插入元素的線程,直到隊列不滿。
  • 支持阻塞的移除方法:意思是在隊列為空時,獲取元素的線程會等待隊列變?yōu)榉强铡?/li>

阻塞隊列常用于生產(chǎn)者和消費者的場景,生產(chǎn)者是向隊列里添加元素的線程,消費者是從隊列里取元素的線程。阻塞隊列就是生產(chǎn)者用來存放元素、消費者用來獲取元素的容器。

列舉幾個常見的阻塞隊列

  • ArrayBlockingQueue:一個由數(shù)組結(jié)構(gòu)組成的有界阻塞隊列。
  • LinkedBlockingQueue:一個由鏈表結(jié)構(gòu)組成的有界阻塞隊列。
  • PriorityBlockingQueue:一個支持優(yōu)先級排序的無界阻塞隊列。
  • DelayQueue:一個使用優(yōu)先級隊列實現(xiàn)的無界阻塞隊列。
  • SynchronousQueue:一個不存儲元素的阻塞隊列。
  • LinkedTransferQueue:一個由鏈表結(jié)構(gòu)組成的無界阻塞隊列。
  • LinkedBlockingDeque:一個由鏈表結(jié)構(gòu)組成的雙向阻塞隊列。

突擊并發(fā)編程JUC系列-阻塞隊列 BlockingQueue

線程池

使用線程池的優(yōu)勢

Java 中的線程池是運用場景最多的并發(fā)框架,幾乎所有需要異步或并發(fā)執(zhí)行任務(wù)的程序都可以使用線程池。

  • 降低資源消耗。 通過重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗。
  • 提高響應(yīng)速度。 當(dāng)任務(wù)到達(dá)時,任務(wù)可以不需要等到線程創(chuàng)建就能立即執(zhí)行。
  • 提高線程的可管理性。 線程是稀缺資源,如果無限制地創(chuàng)建,不僅會消耗系統(tǒng)資源,還會降低系統(tǒng)的穩(wěn)定性,使用線程池可以進行統(tǒng)一分配、調(diào)優(yōu)和監(jiān)控。但是,要做到合理利用線程池,必須對其實現(xiàn)原理了如指掌。

線程池的實現(xiàn)原理

當(dāng)提交一個新任務(wù)到線程池時,線程池的處理流程如下:

  • 線程池判斷核心線程池里的線程是否都在執(zhí)行任務(wù)。如果不是,則創(chuàng)建一個新的工作線程來執(zhí)行任務(wù)。如果核心線程池里的線程都在執(zhí)行任務(wù),則進入下個流程。
  • 線程池判斷工作隊列是否已經(jīng)滿。如果工作隊列沒有滿,則將新提交的任務(wù)存儲在這個工作隊列里。如果工作隊列滿了,則進入下個流程。
  • 線程池判斷線程池的線程是否都處于工作狀態(tài)。如果沒有,則創(chuàng)建一個新的工作線程來執(zhí)行任務(wù)。如果已經(jīng)滿了,則交給飽和策略來處理這個任務(wù)。


ThreadPoolExecutor執(zhí)行execute()方法的示意圖 如下:

ThreadPoolExecutor執(zhí)行execute方法分下面 4 種情況:

  • 1、如果當(dāng)前運行的線程少于corePoolSize,則創(chuàng)建新線程來執(zhí)行任務(wù)(注意,執(zhí)行這一步驟需要獲取全局鎖)。
  • 2、如果運行的線程等于或多于corePoolSize,則將任務(wù)加入BlockingQueue。
  • 3、如果無法將任務(wù)加入BlockingQueue(隊列已滿),則創(chuàng)建新的線程來處理任務(wù)(注意,執(zhí)行這一步驟需要獲取全局鎖)。
  • 4、如果創(chuàng)建新線程將使當(dāng)前運行的線程超出maximumPoolSize,任務(wù)將被拒絕,并調(diào)用RejectedExecutionHandler.rejectedExecution()方法。

ThreadPoolExecutor采取上述步驟的總體設(shè)計思路,是為了在執(zhí)行execute()方法時,盡可能地避免獲取全局鎖(那將會是一個嚴(yán)重的可伸縮瓶頸)。在ThreadPoolExecutor完成預(yù)熱之后(當(dāng)前運行的線程數(shù)大于等于corePoolSize),幾乎所有的execute()方法調(diào)用都是執(zhí)行步驟 2,而步驟2不需要獲取全局鎖。

創(chuàng)建線程有三種方式:

  • 繼承 Thread 重寫 run 方法
  • 實現(xiàn) Runnable 接口
  • 實現(xiàn) Callable 接口 (有返回值)

線程有哪些狀態(tài)?

  • NEW(初始),新建狀態(tài),線程被創(chuàng)建出來,但尚未啟動時的線程狀態(tài);
  • RUNNABLE(就緒狀態(tài)),表示可以運行的線程狀態(tài),它可能正在運行,或者是在排隊等待操作系統(tǒng)給它分配 CPU 資源;
  • BLOCKED(阻塞),阻塞等待鎖的線程狀態(tài),表示處于阻塞狀態(tài)的線程正在等待監(jiān)視器鎖,比如等待執(zhí)行 synchronized 代碼塊或者使用 synchronized 標(biāo)記的方法;
  • WAITING(等待),等待狀態(tài),一個處于等待狀態(tài)的線程正在等待另一個線程執(zhí)行某個特定的動作,比如,一個線程調(diào)用了 Object.wait()方法,那它就在等待另一個線程調(diào)用 Object.notify()Object.notifyAll()方法;
  • TIMED_WAITING(超時等待),計時等待狀態(tài),和等待狀態(tài)(WAITING)類似,它只是多了超時時間,比如調(diào)用了有超時時間設(shè)置的方法 Object.wait(long timeout)Thread.join(long timeout)等這些方法時,它才會進入此狀態(tài);
  • TERMINATED,終止?fàn)顟B(tài),表示線程已經(jīng)執(zhí)行完成。

線程池的狀態(tài)有那些?

  • running:這是最正常的狀態(tài),接受新的任務(wù),處理等待隊列中的任務(wù)。
  • shutdown:不接受新的任務(wù)提交,但是會繼續(xù)處理等待隊列中的任務(wù)。
  • stop:不接受新的任務(wù)提交,不再處理等待隊列中的任務(wù),中斷正在執(zhí)行任務(wù)的線程。
  • tidying:所有的任務(wù)都銷毀了,workcount 為 0,線程池的狀態(tài)再轉(zhuǎn)換 tidying 狀態(tài)時,會執(zhí)行鉤子方法 terminated()。
  • terminatedterminated() 方法結(jié)束后,線程池的狀態(tài)就會變成這個。

線程池中 sumbit() 和 execute() 方法有什么區(qū)別?

  • execute(): 只能執(zhí)行 Runable 類型的任務(wù)。
  • submit() 可以執(zhí)行 RunableCallable類型的任務(wù)。

? Callable 類型的任務(wù)可以獲取執(zhí)行的返回值,而 Runnable 執(zhí)行無返回值。

線程池創(chuàng)建的方式

  • newSingleThreadExecutor(): 他的特點是在于線程數(shù)目被限制位1:操作一個無界的工作隊列,所以它保證了所有的任務(wù)的都是順序執(zhí)行,最多會有一個任務(wù)處于活動狀態(tài),并且不允許使用者改動線程池實例,因此可以避免其改變線程數(shù)目。
  • newCachedThreadPool():它是一種用來處理大量短時間工作任務(wù)的線程,具有幾個鮮明的特點,它會試圖緩存線程并重用,當(dāng)無緩存線程可用時,就會創(chuàng)建新的工作線程,如果線程閑置的時間超過 60 秒,則被終止并移除緩存;長時間閑置時,這種線程池不會消耗什么資源,其內(nèi)部使用 synchronousQueue 作為工作隊列。
  • newFixedThreadPool(int nThreads) :重用指定數(shù)目 nThreads 的線程,其背后使用的無界的工作隊列,任何時候最后有 nThreads 個工作線程活動的,這意味著 如果任務(wù)數(shù)量超過了活動隊列數(shù)目,將在工作隊列中等待空閑線程出現(xiàn),如果有工作線程退出,將會有新的工作線程被創(chuàng)建,以補足指定的數(shù)目 nThreads。
  • newSingleThreadScheduledExecutor(): 創(chuàng)建單線程池,返回ScheduleExecutorService 可以進行定時或周期性的工作強度。
  • newScheduleThreadPool(int corePoolSize): 和 newSingleThreadSceduleExecutor() 類似,創(chuàng)建的ScheduledExecutorService可以進行定時或周期的工作調(diào)度,區(qū)別在于單一工作線程還是工作線程。
  • newWorkStrealingPool(int parallelism):這是一個經(jīng)常被人忽略的線程池,Java 8 才加入這個創(chuàng)建方法,其內(nèi)部會構(gòu)建ForkJoinPool利用 work-strealing 算法 并行的處理任務(wù),不保證處理順序。
  • ThreadPollExecutor : 是最原始的線程池創(chuàng)建,上面 1-3 創(chuàng)建方式 都是對ThreadPoolExecutor 的封裝。

上面 7 種創(chuàng)建方式中,前 6 種 通過Executors工廠方法創(chuàng)建,ThreadPoolExecutor 手動創(chuàng)建。

ThreadPollExecutor 構(gòu)造方法

下面介紹下 ThreadPoolExecutor 接收 7 個參數(shù)的構(gòu)造方法

/**
     * 用給定的初始參數(shù)創(chuàng)建一個新的ThreadPoolExecutor。
     */
    public ThreadPoolExecutor(int corePoolSize,//線程池的核心線程數(shù)量
                              int maximumPoolSize,//線程池的最大線程數(shù)
                              long keepAliveTime,//當(dāng)線程數(shù)大于核心線程數(shù)時,多余的空閑線程存活的最長時間
                              TimeUnit unit,//時間單位
                              BlockingQueue<Runnable> workQueue,//任務(wù)隊列
                              ThreadFactory threadFactory,//線程工廠
                              RejectedExecutionHandler handler//拒絕策略
                               )
  • corePoolSize : 核心線程數(shù)線程數(shù)定義了最小可以同時運行的線程數(shù)量。
  • maximumPoolSize : 當(dāng)隊列中存放的任務(wù)達(dá)到隊列容量的時候,當(dāng)前可以同時運行的線程數(shù)量變?yōu)樽畲缶€程數(shù)。
  • workQueue: 當(dāng)新任務(wù)來的時候會先判斷當(dāng)前運行的線程數(shù)量是否達(dá)到核心線程數(shù),如果達(dá)到的話,信任就會被存放在隊列中。
  • keepAliveTime:線程活動保持時間,當(dāng)線程池中的線程數(shù)量大于 corePoolSize 的時候,如果這時沒有新的任務(wù)提交,核心線程外的線程不會立即銷毀,而是會等待,直到等待的時間超過了 keepAliveTime才會被回收銷毀;
  • unit : keepAliveTime 參數(shù)的時間單位。
  • threadFactory : 任務(wù)隊列,用于保存等待執(zhí)行的任務(wù)的阻塞隊列??梢赃x擇以下幾個阻塞隊列。
    • ArrayBlockingQueue:是一個基于數(shù)組結(jié)構(gòu)的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。
    • LinkedBlockingQueue:一個基于鏈表結(jié)構(gòu)的阻塞隊列,此隊列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。靜態(tài)工廠方法Executors.newFixedThreadPool()使用了這個隊列。
    • SynchronousQueue:一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調(diào)用移除操作,否則插入操作一直處于阻塞狀態(tài),吞吐量通常要高于Linked-BlockingQueue,靜態(tài)工廠方法Executors.newCachedThreadPool使用了這個隊列。
    • PriorityBlockingQueue:一個具有優(yōu)先級的無限阻塞隊列。
  • handler :飽和策略(又稱拒絕策略)。當(dāng)隊列和線程池都滿了,說明線程池處于飽和狀態(tài),那么必須采取一種策略處理提交的新任務(wù)。這個策略默認(rèn)情況下是AbortPolicy,表示無法處理新任務(wù)時拋出異常。在JDK 1.5 中 Java 線程池框架提供了以下4種策略。
    • AbortPolicy:直接拋出異常。
    • CallerRunsPolicy:只用調(diào)用者所在線程來運行任務(wù)。
    • DiscardOldestPolicy:丟棄隊列里最近的一個任務(wù),并執(zhí)行當(dāng)前任務(wù)。
    • DiscardPolicy:不處理,丟棄掉

歡迎關(guān)注公眾號 山間木匠 , 我是小春哥,從事 Java 后端開發(fā),會一點前端、通過持續(xù)輸出系列技術(shù)文章以文會友,如果本文能為您提供幫助,歡迎大家關(guān)注、在看、 點贊、分享支持,我們下期再見!<br />

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容