Flash圖解執行緒池 | 阿里巴巴面試官喜歡問的執行緒池到底是什麼?

CoderW喜歡寫部落格發表於2021-02-01

前言

前幾天小強去阿里巴巴面試Java崗,止步於二面。

他和我訴苦自己被虐的多慘多慘,特別是深挖執行緒和執行緒池的時候,居然被問到不知道如何作答。

對於他的遭遇,結合他過了一面的那個嘚瑟樣,我深表同情(加大力度)!

我差點笑出了聲

好了,不開玩笑了,在和小強的面試題中,我選取了幾個比較典型的執行緒和執行緒池的問題。

Java中的執行緒和作業系統的執行緒有什麼關係?

呼叫start方法是如何執行run方法的?

執行緒池提交任務有哪幾種方式?分別有什麼區別?

談談你對阻塞佇列的理解。

常見的執行緒池有哪些?為什麼阿里不允許使用 Executors 去建立執行緒池?

執行緒池任務排程的流程大致講一下。

執行緒池裡面的執行緒執行異常了會怎麼樣?

核心執行緒和非核心執行緒是如何區分的?

想要答對這些問題,並不是很難,但是想要答好,我覺得是非常考驗個人功底的。

為了弄清這些問題,我連夜加急,採訪了“執行緒”,下面是執行緒的自述。

我是誰

我是一個執行緒,一個底層的打工人。

打工人

總有人把我和程式搞混,但其實我和程式的區別很大。

程式是程式的一次執行,CPU的資源都是分發給程式而不是分發給我們執行緒,程式是資源分配的最小單位,一個程式可以包含很多向我這樣的執行緒。

我們執行緒是CPU排程執行的最小單位,真正的打工人。

Java中的執行緒

在Java裡面,我的名字叫做java.lang.Thread。

需要注意的是,呼叫run方法和執行一個普通方法沒有區別。想要真正的建立一個執行緒並啟動,需要呼叫我的start方法。

f0b45f3125b59f52e34f562240e9a1ff

有一點我必須告訴你,就是我也是有小弟的。

在JVM裡面,我有一個JavaThread的小弟,他幫我聯絡作業系統的osthread執行緒。

呼叫我的start方法之後,具體的執行流程是這樣的:

image-20210130184446156

當然了,這個過程省略了很多細節,不過很明確的是,我和核心執行緒是一一對應的。

排程我就相當於排程核心執行緒,而排程核心執行緒需要在使用者態和核心態之間切換,這個過程開銷是非常大的。

所以,建立我成本是很高的,一定要慎重。

Untitled Diagram

執行緒池

和你們人類一樣,我也有著精彩的一生,也會經歷出生(建立)、奮鬥(Running)、死亡(銷燬)等過程,今天我主要和你講述的是我打工奮鬥的生活。

原來我是打零工的,有人需要我的時候就建立一個我,等我完成工作就把我銷燬。

image-20210130172133624

上面也提到過,我和核心執行緒是一對一的,建立和銷燬的過程是非常消耗資源的,所以這樣的成本非常高。

於是,有人就想了一個辦法,開了一個公司,也就是你們說的執行緒池。

執行緒池公司統一管理排程我們執行緒。我們線上程池裡面重復著等待工作——完成工作的步驟。

image-20210130175632614

這樣我就可以日復一日年復一年的重複打工了,這種提供了減少物件數量從而改善應用所需的物件結構的方式的模式,被你們人類叫做“享元模式”。

執行緒池公司有很多種,但都離不開這幾個主要指標:

image-20210130175721394
  • corePoolSize:公司正式員工人數。
  • maximumPoolSize:正式工+臨時工最大數量。
  • keepAliveTime:臨時工多久沒做事情會被開除。
  • unit:臨時工沒做事情會被開除的時間單位。
  • workQueue:公司業務接收部門。
  • threadFactory:行政部,負責招聘培訓員工的。
  • handler:業務部接收業務到達上限了的處理方式。

阻塞佇列

執行緒池中的workQueue是一個阻塞佇列,用於存放執行緒池未能及時處理執行的任務。

它的存在既解耦了任務的提交與執行,又能起到一個緩衝的作用。

阻塞佇列有很多,下面我帶你瞭解一下常見的阻塞佇列。

ArrayBlockingQueue

基於陣列實現的有界阻塞佇列,建立的時候需要指定容量。此型別的佇列按照FIFO(先進先出)的規則對元素進行排序。

ArrayBlockingQueue

LinkedBlockingQueue

基於連結串列實現阻塞佇列,預設大小為Integer.MAX_VALUE。按照FIFO(先進先出)的規則對元素進行排序

LinkedBlockingQueue

SynchronousQueue

一個不儲存元素的阻塞佇列。每一個put操作必須阻塞等待其他執行緒的take操作,take操作也必須等待其他執行緒的put操作。

SynchronousQueue_2

PriorityBlockingQueue

一個基於陣列利用堆結構實現優先順序效果的無界佇列,預設自然序排序,也可以自己實現compareTo方法自定義排序規則。

PriorityBlockingQueue

DelayedWorkQueue

一個實現了優先順序佇列功能且實現了延遲獲取的無界佇列,在建立元素時,可以指定多久多久才能在佇列中獲取當前元素。只有延時期滿了後才能從佇列中獲取元素。

DelayedWorkQueue

拒絕策略

當任務佇列滿了之後,如果還有任務提交過來,會觸發拒絕策略,常見的拒絕策略有:

  • AbortPolicy:丟棄任務並丟擲異常,預設該方式。

  • CallerRunsPolicy:由呼叫執行緒自己處理該任務。誰呼叫,誰處理。

image-20210131164350267
  • DiscardPolicy:丟棄任務,但是不丟擲異常。

  • DiscardOldestPolicy:拋棄任務佇列中最舊的任務也就是最先加入佇列的,再把這個新任務新增進去。先從任務佇列中彈出最先加入的任務,空出一個位置,然後再次執行execute方法把任務加入佇列。

image-20210131164701335

當然,除了以上這幾種拒絕策略,你也可以根據實際的業務場景和業務需求去自定義拒絕策略,只需要實現RejectedExecutionHander介面,自定義裡面的rejectedExecution方法。

執行流程

我們每個執行緒會被包裝成Worker,執行緒池裡面有一個HashSet存放Worker。

當有任務提交過來之後:

  1. 首先檢測執行緒池執行狀態,如果不是RUNNING,則直接拒絕,執行緒池要保證在RUNNING的狀態下執行任務。
  2. 如果執行緒池中Worker的數量小於核心執行緒數,就會去建立一個新的執行緒,也就是招聘一個正式工讓他執行任務。
  3. 如果Worker的數量大於或者等於核心執行緒數,就會把任務放到阻塞任務佇列裡面。
  4. 如果任務佇列滿了還有任務過來,如果臨時工名額沒有滿(workerCount < maximumPoolSize),就去招聘臨時工讓臨時工執行任務。如果臨時工名額都滿了,觸發任務拒絕策略。
image-20210131184333322

總結而言,就是核心執行緒能幹的事情儘量不去建立非核心執行緒,這是執行緒池很關鍵的一點。

new ThreadPoolExecutor(4,  8, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(4));

以這個執行緒池為例,下面是他的任務提交和執行流程:

恢復_執行緒池_3

有哪些執行緒池

我有過四段工作經歷,每段經歷都有著精彩的故事。

SingleThreadExecutor

SingleThreadExecutor是我加入的第一家執行緒池,這是一家創業公司,整個執行緒池就只有我一個執行緒。

image-20210131022002342
SingleThreadExecutor_2

所有的任務都由我幹,而且任務佇列是一個無界佇列。就是說,打工的執行緒只有我一個,但是需求任務可以是無限多。

在需求任務很多的時候,經常出現任務處理不過來的情況,導致任務堆積,出現OOM。

但因為所有的活都是我幹,沒有繁瑣的溝通成本,不需要處理執行緒同步的問題,這算是這種執行緒池的一個優點吧。

這種執行緒池適用於併發量不大且需要任務順序執行的場景。

FixedThreadPool

後來公司倒閉了,我又加入了一個叫FixedThreadPool的執行緒池。

image-20210131021901279
FixedThreadPool

FixedThreadPool和SingleThreadExecutor唯一不同的地方就是核心執行緒的數量,FixedThreadPool可以招收很多的打工執行緒。

在這裡,我不再是孤軍奮鬥了,我有了一群共同打拼的小夥伴,大家一起完成任務,一起承擔壓力。

可這種執行緒池還是存在一個問題——任務佇列是無界的,需求任務過多的話,還是會造成OOM。

這種執行緒池執行緒數固定,且不被回收,執行緒與執行緒池的生命週期同步的執行緒池,適用於任務量比較固定但耗時長的任務。

CachedThreadPool

後來,為了離家更近,我離職了。加入了一家叫CachedThreadPool的執行緒池,進去之後,卻發現這是一家外包公司。

image-20210131015429257
CachedThreadPool

這種執行緒池裡面沒有一個核心執行緒(正式工),一有需求就去招聘一個非核心執行緒(臨時工)。

如果一個執行緒任務幹完了之後,60秒之後沒有新的任務就會被辭退。

這種執行緒池的任務佇列採用的是SynchronousQueue,這個佇列是無法插入任務的,一有任務就建立一個執行緒執行,如果併發高且任務耗時長,建立太多執行緒也是可能導致OOM的。所以CachedThreadPool比較適合任務量大但耗時少的任務。

ScheduleThreadPool

經歷了外面的風風雨雨,我覺得還是找份固定的工作比較可靠,於是我加入了一家叫做ScheduleThreadPool的國企。

image-20210131022031076
ScheduleThreadPool

在這裡,工作比較的輕鬆,多數情況下,我只需要在固定的時間幹固定的活。

任務忙不過來的時候,公司也會招聘一些臨時工幫忙處理,臨時工幹完活就會被辭退。

綜合來說,這類執行緒池適用於執行定時任務和具體固定週期的重複任務。由於採用的任務佇列是DelayedWorkQueue無界佇列,所以也是有OOM的風險的。

總結

好了,關於執行緒的故事就告一段落了。關於執行緒池的應用實踐,我們下次再聊。

文章開頭的面試題在大部分在文中都能找到答案,對於沒有提到的,這裡做一個補充:

1. 執行緒池提交任務有哪幾種方式?分別有什麼區別?

有execute和submit兩種方式

  • execute只能提交Runnable型別的任務,無返回值。submit既可以提交Runnable型別的任務,也可以提交Callable型別的任務,會有一個型別為Future的返回值,但當任務型別為Runnable時,返回值為null。

  • execute在執行任務時,如果遇到異常會直接丟擲,而submit不會直接丟擲,只有在使用Future的get方法獲取返回值時,才會丟擲異常。

2. 執行緒池裡面的執行緒執行異常了會怎麼樣?

如果一個執行緒執行任務的過程中出現異常,那麼這個執行緒對應的Worker會被移出執行緒池,該執行緒也會被銷燬回收。

同時會通過指定的執行緒工廠建立一個執行緒,並封裝成Worker放入執行緒池代替移除的Worker。

image-20210201045911590

3. 核心執行緒能被回收嗎?

核心執行緒預設不會被回收。但是可以呼叫allowCoreThreadTimeOut讓核心執行緒可以被回收。

image-20210131190101003

需要注意的是,呼叫這個方法的執行緒池必須將keepAliveTime設定為大於0,否則會丟擲異常。

image-20210131190226646

4. 核心執行緒和非核心執行緒是如何區分的?

核心執行緒和非核心執行緒是一個抽象概念,只是用於更好的表述執行緒池的執行邏輯,實際上都對應作業系統的osThread,都是重量級執行緒。

在新增Worker的時候,通過一個boolean表達是核心執行緒還是非核心執行緒,本質上兩者沒有什麼不同。

image-20210131205854199

5. 為什麼阿里不允許使用 Executors 去建立執行緒池?

FixedThreadPool 和 SingleThreadPool:允許的請求佇列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。

CachedThreadPool:允許的建立執行緒數量為 Integer.MAX_VALUE,可能會建立大量的執行緒,從而導致 OOM。

總結來說就是,使用Executors建立執行緒池會容易忽視執行緒池的一些屬性,使用不當容易引起資源耗盡。

寫在最後

這個世界上或許沒有執行緒,又或許人人都是執行緒。

無畏年少青春,迎風瀟灑前行,做一個努力奮鬥的執行緒,希望他日回首望去,不是一片黑暗,而是漫天星光。

好了,今天的文章就到這裡了。

最後,感謝你的閱讀!

我是CoderW,一個普通的程式設計師。

點個關注,我們下期再見!

image-20210131205854199

相關文章