【高併發】關於執行緒池,螞蟻金服面試官問了我這些內容!!

冰河團隊發表於2020-07-27

寫在前面

最近,一名讀者去螞蟻金服面試,面試官問了他關於樂觀鎖和悲觀鎖的問題,幸虧他看了我的【高併發專題】文章,結果是替這名讀者高興!現就部分面試題目總結成文,供小夥伴們參考。

小夥伴們可以關注 冰河技術 微信公眾號來學習【高併發專題】,學習超硬核知識技能,跳槽大廠,升級加薪,指日可待!

面試彙總

Java中的執行緒池是如何實現的?

在Java中,所謂的執行緒池中的“執行緒”,其實是被抽象為了一個靜態內部類Worker,它基於AQS實現,存放線上程池的HashSetworkers成員變數中;而需要執行的任務則存放在成員變數workQueue(BlockingQueueworkQueue)中。這樣,整個執行緒池實現的基本思想就是、從workQueue中不斷取出需要執行的任務,放在Workers中進行處理。

建立執行緒池的幾個核心構造引數?

Java中的執行緒池的建立其實非常靈活,我們可以通過配置不同的引數,建立出行為不同的執行緒池,這幾個引數包括:

  • corePoolSize:執行緒池的核心執行緒數。
  • maximumPoolSize:執行緒池允許的最大執行緒數。
  • keepAliveTime:超過核心執行緒數時閒置執行緒的存活時間。
  • workQueue:任務執行前儲存任務的佇列,儲存由execute方法提交的Runnable任務。

執行緒池中的執行緒是怎麼建立的?是一開始就隨著執行緒池的啟動建立好的嗎?

顯然不是的。執行緒池預設初始化後不啟動Worker,等待有請求時才啟動。每當我們呼叫execute()方法新增一個任務時,執行緒池會做如下判斷:

如果正在執行的執行緒數量小於corePoolSize,那麼馬上建立執行緒執行這個任務;

如果正在執行的執行緒數量大於或等於corePoolSize,那麼將這個任務放入佇列;

如果這時候佇列滿了,而且正在執行的執行緒數量小於maximumPoolSize,那麼還是要建立非核心執行緒立刻執行這個任務;

如果佇列滿了,而且正在執行的執行緒數量大於或等於maximumPoolSize,那麼執行緒池會丟擲異常RejectExecutionException。

當一個執行緒完成任務時,它會從佇列中取下一個任務來執行。當一個執行緒無事可做,超過一定的時間(keepAliveTime)時,執行緒池會判斷。如果當前執行的執行緒數大於corePoolSize,那麼這個執行緒就被停掉。所以執行緒池的所有任務完成後,它最終會收縮到corePoolSize的大小。

既然提到可以通過配置不同引數建立出不同的執行緒池,那麼Java中預設實現好的執行緒池又有哪些呢?請比較它們的異同。

(1)SingleThreadExecutor執行緒池這個執行緒池只有一個核心執行緒在工作,也就是相當於單執行緒序列執行所有任務。如果這個唯一的執行緒因為異常結束,那麼會有一個新的執行緒來替代它。此執行緒池保證所有任務的執行順序按照任務的提交順序執行。

corePoolSize:1,只有一個核心執行緒在工作。
maximumPoolSize:1。
keepAliveTime:0L。
workQueue:newLinkedBlockingQueue<Runnable>(),其緩衝佇列是無界的。

(2)FixedThreadPool執行緒池

FixedThreadPool是固定大小的執行緒池,只有核心執行緒。每次提交一個任務就建立一個執行緒,直到執行緒達到執行緒池的最大大小。執行緒池的大小一旦達到最大值就會保持不變,如果某個執行緒因為執行異常而結束,那麼執行緒池會補充一個新執行緒。FixedThreadPool多數針對一些很穩定很固定的正規併發執行緒,多用於伺服器。

corePoolSize:nThreads
maximumPoolSize:nThreads
keepAliveTime:0L
workQueue:newLinkedBlockingQueue<Runnable>(),其緩衝佇列是無界的。

(3)CachedThreadPool執行緒池CachedThreadPool是無界執行緒池,如果執行緒池的大小超過了處理任務所需要的執行緒,那麼就會回收部分空閒(60秒不執行任務)執行緒,當任務數增加時,此執行緒池又可以智慧的新增新執行緒來處理任務。

執行緒池大小完全依賴於作業系統(或者說JVM)能夠建立的最大執行緒大小。SynchronousQueue是一個是緩衝區為1的阻塞佇列。
快取型池子通常用於執行一些生存期很短的非同步型任務,因此在一些面向連線的daemon型SERVER中用得不多。但對於生存期短的非同步任務,它是Executor的首選。

corePoolSize:0
maximumPoolSize:Integer.MAX_VALUE
keepAliveTime:60L
workQueue:newSynchronousQueue<Runnable>(),一個是緩衝區為1的阻塞佇列。

(4)ScheduledThreadPool執行緒池ScheduledThreadPool、核心執行緒池固定,大小無限的執行緒池。此執行緒池支援定時以及週期性執行任務的需求。建立一個週期性執行任務的執行緒池。如果閒置,非核心執行緒池會在DEFAULT_KEEPALIVEMILLIS時間內回收。

corePoolSize:corePoolSize
maximumPoolSize:Integer.MAX_VALUE
keepAliveTime:DEFAULT_KEEPALIVE_MILLIS
workQueue:newDelayedWorkQueue()

5、如何在Java執行緒池中提交執行緒?

執行緒池最常用的提交任務的方法有兩種:

  • execute()、ExecutorService.execute方法接收一個Runable例項,它用來執行一個任務。
  • submit()、ExecutorService.submit()方法返回的是Future物件。可以用isDone()來查詢Future是否已經完成,當任務完成時,它具有一個結果,可以呼叫get()來獲取結果。也可以不用isDone()進行檢查就直接呼叫get(),在這種情況下,get()將阻塞,直至結果準備就緒。Java記憶體模型相關問題

什麼是Java的記憶體模型,Java中各個執行緒是怎麼彼此看到對方的變數的?

Java的記憶體模型定義了程式中各個變數的訪問規則,即在虛擬機器中將變數儲存到記憶體和從記憶體中取出這樣的底層細節。此處的變數包括例項欄位、靜態欄位和構成陣列物件的元素,但是不包括區域性變數和方法引數,因為這些是執行緒私有的,不會被共享,所以不存在競爭問題。

Java中各個執行緒是怎麼彼此看到對方的變數的呢?
Java中定義了主記憶體與工作記憶體的概念、所有的變數都儲存在主記憶體,每條執行緒還有自己的工作記憶體,儲存了被該執行緒使用到的變數的主記憶體副本拷貝。

執行緒對變數的所有操作(讀取、賦值)都必須在工作記憶體中進行,不能直接讀寫主記憶體的變數。不同的執行緒之間也無法直接訪問對方工作記憶體的變數,執行緒間變數值的傳遞需要通過主記憶體。

請談談volatile有什麼特點,為什麼它能保證變數對所有執行緒的可見性?

關鍵字volatile是Java虛擬機器提供的最輕量級的同步機制。當一個變數被定義成volatile之後,具備兩種特性、

(1)保證此變數對所有執行緒的可見性。當一條執行緒修改了這個變數的值,新值對於其他執行緒是可以立即得知的。而普通變數做不到這一點。
(2)禁止指令重排序優化。普通變數僅僅能保證在該方法執行過程中,得到正確結果,但是不保證程式程式碼的執行順序。Java的記憶體模型定義了8種記憶體間操作、lock和unlock把一個變數標識為一條執行緒獨佔的狀態。把一個處於鎖定狀態的變數釋放出來,釋放之後的變數才能被其他執行緒鎖定。read和write把一個變數值從主記憶體傳輸到執行緒的工作記憶體,以便load。把store操作從工作記憶體得到的變數的值,放入主記憶體的變數中。

load和store把read操作從主記憶體得到的變數值放入工作記憶體的變數副本中。把工作記憶體的變數值傳送到主記憶體,以便write。use和assgin把工作記憶體變數值傳遞給執行引擎。將執行引擎值傳遞給工作記憶體變數值。volatile的實現基於這8種記憶體間操作,保證了一個執行緒對某個volatile變數的修改,一定會被另一個執行緒看見,即保證了可見性。

既然volatile能夠保證執行緒間的變數可見性,是不是就意味著基於volatile變數的運算就是併發安全的?

顯然不是的。基於volatile變數的運算在併發下不一定是安全的。volatile變數在各個執行緒的工作記憶體,不存在一致性問題(各個執行緒的工作記憶體中volatile變數,每次使用前都要重新整理到主記憶體)。但是Java裡面的運算並非原子操作,導致volatile變數的運算在併發下一樣是不安全的。

請對比下volatile對比Synchronized的異同。

Synchronized既能保證可見性,又能保證原子性,而volatile只能保證可見性,無法保證原子性。

ThreadLocal和Synchonized都用於解決多執行緒併發訪問,防止任務在共享資源上產生衝突。但是ThreadLocal與Synchronized有本質的區別。

Synchronized用於實現同步機制,是利用鎖的機制使變數或程式碼塊在某一時該只能被一個執行緒訪問,是一種“以時間換空間”的方式。而ThreadLocal為每一個執行緒都提供了變數的副本,使得每個執行緒在某一時間訪問到的並不是同一個物件,根除了對變數的共享,是一種“以空間換時間”的方式。

請談談ThreadLocal是怎麼解決併發安全的?

ThreadLocal這是Java提供的一種儲存執行緒私有資訊的機制,因為其在整個執行緒生命週期內有效,所以可以方便地在一個執行緒關聯的不同業務模組之間傳遞資訊,比如事務ID、Cookie等上下文相關資訊。ThreadLocal為每一個執行緒維護變數的副本,把共享資料的可見範圍限制在同一個執行緒之內,其實現原理是,在ThreadLocal中有一個Map,用於儲存每一個執行緒的變數的副本。

很多人都說要慎用ThreadLocal,談談你的理解,使用ThreadLocal需要注意些什麼?

使用ThreadLocal要注意remove!ThreadLocal的實現是基於一個所謂的ThreadLocalMap,在ThreadLocalMap中,它的key是一個弱引用。通常弱引用都會和引用佇列配合清理機制使用,但是ThreadLocal是個例外,它並沒有這麼做。這意味著,廢棄專案的回收依賴於顯式地觸發,否則就要等待執行緒結束,進而回收相應ThreadLocalMap!這就是很多OOM的來源,所以通常都會建議,應用一定要自己負責remove,並且不要和執行緒池配

重磅福利

關注「 冰河技術 」微信公眾號,後臺回覆 “設計模式” 關鍵字領取《深入淺出Java 23種設計模式》PDF文件。回覆“Java8”關鍵字領取《Java8新特性教程》PDF文件。兩本PDF均是由冰河原創並整理的超硬核教程,面試必備!!

好了,今天就聊到這兒吧!別忘了點個贊,給個在看和轉發,讓更多的人看到,一起學習,一起進步!!

寫在最後

如果你覺得冰河寫的還不錯,請微信搜尋並關注「 冰河技術 」微信公眾號,跟冰河學習高併發、分散式、微服務、大資料、網際網路和雲原生技術,「 冰河技術 」微信公眾號更新了大量技術專題,每一篇技術文章乾貨滿滿!不少讀者已經通過閱讀「 冰河技術 」微信公眾號文章,吊打面試官,成功跳槽到大廠;也有不少讀者實現了技術上的飛躍,成為公司的技術骨幹!如果你也想像他們一樣提升自己的能力,實現技術能力的飛躍,進大廠,升職加薪,那就關注「 冰河技術 」微信公眾號吧,每天更新超硬核技術乾貨,讓你對如何提升技術能力不再迷茫!

相關文章