阿里Java資深架構師詳解大廠多執行緒面試題,想進大廠這些你不得不知

慕容千語發表於2018-11-27


阿里Java資深架構師詳解大廠多執行緒面試題,想進大廠這些你不得不知

多執行緒、執行緒池

阿里Java資深架構師詳解大廠多執行緒面試題,想進大廠這些你不得不知

多執行緒是實現併發機制的一種有效手段。程式和執行緒一樣,都是實現併發的一個基本單位。執行緒是比程式更小的執行單位,執行緒是程式的基礎之上進行進一步的劃分。所謂多執行緒是指一個程式在執行過程中可以產生多個更小的程式單元,這些更小的單元稱為執行緒,這些執行緒可以同時存在,同時執行,一個程式可能包含多個同時執行的執行緒。程式與執行緒的區別如圖所示:

阿里Java資深架構師詳解大廠多執行緒面試題,想進大廠這些你不得不知

多執行緒面試題及答案

這裡例舉20道面試題以及答案,由於篇幅過長,就不一一發出答案了,如果有需要的話可以參考結尾的獲取方式。

阿里Java資深架構師詳解大廠多執行緒面試題,想進大廠這些你不得不知

1.多執行緒有什麼用?

1)發揮多核CPU 的優勢

隨著工業的進步,現在的筆記本、桌上型電腦乃至商用的應用伺服器至少也都是雙核的,4 核、8 核甚至 16 核的也都不少見,如果是單執行緒的程式,那麼在雙核 CPU 上就浪費了 50%, 在 4 核 CPU 上就浪費了 75%。單核 CPU 上所謂的"多執行緒"那是假的多執行緒,同一時間處理器只會處理一段邏輯,只不過執行緒之間切換得比較快,看著像多個執行緒"同時"執行罷了。多核 CPU 上的多執行緒才是真正的多執行緒,它能讓你的多段邏輯同時工作,多執行緒,可以真正發揮出多核CPU 的優勢來,達到充分利用CPU 的目的。

2)防止阻塞

從程式執行效率的角度來看,單核 CPU 不但不會發揮出多執行緒的優勢,反而會因為在單核CPU 上執行多執行緒導致執行緒上下文的切換,而降低程式整體的效率。但是單核 CPU 我們還是要應用多執行緒,就是為了防止阻塞。試想,如果單核 CPU 使用單執行緒,那麼只要這個執行緒阻塞了,比方說遠端讀取某個資料吧,對端遲遲未返回又沒有設定超時時間,那麼你的整個程式在資料返回回來之前就停止執行了。多執行緒可以防止這個問題,多條執行緒同時執行,哪怕一條執行緒的程式碼執行讀取資料阻塞,也不會影響其它任務的執行。

3)便於建模

這是另外一個沒有這麼明顯的優點了。假設有一個大的任務 A,單執行緒程式設計,那麼就要考慮很多,建立整個程式模型比較麻煩。但是如果把這個大的任務 A 分解成幾個小任務,任務B、任務 C、任務 D,分別建立程式模型,並通過多執行緒分別執行這幾個任務,那就簡單很多了。

2.執行緒和程式的區別是什麼?

程式和執行緒的主要差別在於它們是不同的作業系統資源管理方式。程式有獨立的地址空間,一個程式崩潰後,在保護模式下不會對其它程式產生影響,而執行緒只是一個程式中的不同執行路徑。執行緒有自己的堆疊和區域性變數,但執行緒之間沒有單獨的地址空間,一個執行緒死掉就等於整個程式死掉,所以多程式的程式要比多執行緒的程式健壯,但在程式切換時,耗費資源較大,效率要差一些。但對於一些要求同時進行並且又要共享某些變數的併發操作,只能用執行緒,不能用程式。

3.Java 實現執行緒有哪幾種方式?

1)繼承 Thread 類實現多執行緒

2)實現 Runnable 介面方式實現多執行緒

3)使用 ExecutorService、Callable、Future 實現有返回結果的多執行緒

4.啟動執行緒方法 start()和 run()有什麼區別?

只有呼叫了 start()方法,才會表現出多執行緒的特性,不同執行緒的 run()方法裡面的程式碼交替執行。如果只是呼叫 run()方法,那麼程式碼還是同步執行的,必須等待一個執行緒的 run()方法裡面的程式碼全部執行完畢之後,另外一個執行緒才可以執行其 run()方法裡面的程式碼。

5.怎麼終止一個執行緒?如何優雅地終止執行緒?

stop 終止,不推薦。

6.一個執行緒的生命週期有哪幾種狀態?它們之間如何流轉的?

NEW:毫無疑問表示的是剛建立的執行緒,還沒有開始啟動。

RUNNABLE: 表示執行緒已經觸發 start()方式呼叫,執行緒正式啟動,執行緒處於執行中狀態。

BLOCKED:表示執行緒阻塞,等待獲取鎖,如碰到 synchronized、lock 等關鍵字等佔用臨界區的情況,一旦獲取到鎖就進行 RUNNABLE 狀態繼續執行。

WAITING:表示執行緒處於無限制等待狀態,等待一個特殊的事件來重新喚醒,如通過wait()方法進行等待的執行緒等待一個 notify()或者 notifyAll()方法,通過 join()方法進行等待的執行緒等待目標執行緒執行結束而喚醒,一旦通過相關事件喚醒執行緒,執行緒就進入了 RUNNABLE 狀態繼續執行。

TIMED_WAITING:表示執行緒進入了一個有時限的等待,如 sleep(3000),等待 3 秒後執行緒重新進行 RUNNABLE 狀態繼續執行。

TERMINATED:表示執行緒執行完畢後,進行終止狀態。需要注意的是,一旦執行緒通過 start 方法啟動後就再也不能回到初始 NEW 狀態,執行緒終止後也不能再回到 RUNNABLE 狀態

7.執行緒中的 wait()和 sleep()方法有什麼區別?

這個問題常問,sleep 方法和 wait 方法都可以用來放棄 CPU 一定的時間,不同點在於如果執行緒持有某個物件的監視器,sleep 方法不會放棄這個物件的監視器,wait 方法會放棄這個物件的監視器

阿里Java資深架構師詳解大廠多執行緒面試題,想進大廠這些你不得不知

8.多執行緒同步有哪幾種方法?

Synchronized 關鍵字,Lock 鎖實現,分散式鎖等。

9.什麼是死鎖?如何避免死鎖?

死鎖就是兩個執行緒相互等待對方釋放物件鎖。

10.多執行緒之間如何進行通訊?

wait/notify

11、執行緒怎樣拿到返回結果?

實現Callable 介面。

12、violatile 關鍵字的作用?

一個非常重要的問題,是每個學習、應用多執行緒的 Java 程式設計師都必須掌握的。理解 volatile關鍵字的作用的前提是要理解 Java 記憶體模型,這裡就不講 Java 記憶體模型了,可以參見第31 點,volatile 關鍵字的作用主要有兩個:

1)多執行緒主要圍繞可見性和原子性兩個特性而展開,使用 volatile 關鍵字修飾的變數,保證了其在多執行緒之間的可見性,即每次讀取到 volatile 變數,一定是最新的資料

2)程式碼底層執行不像我們看到的高階語言----Java 程式這麼簡單,它的執行是 Java 程式碼-->位元組碼-->根據位元組碼執行對應的 C/C++程式碼-->C/C++程式碼被編譯成組合語言-->和硬體電路互動,現實中,為了獲取更好的效能 JVM 可能會對指令進行重排序,多執行緒下可能會出現一些意想不到的問題。使用 volatile 則會對禁止語義重排序,當然這也一定程度上降低了程式碼執行效率從實踐角度而言,volatile 的一個重要作用就是和 CAS 結合,保證了原子性,詳細的可以參見 java.util.concurrent.atomic 包下的類,比如 AtomicInteger。

13、新建 T1、T2、T3 三個執行緒,如何保證它們按順序執行?

用 join 方法。

14、怎麼控制同一時間只有 3 個執行緒執行?

用 Semaphore。

15、為什麼要使用執行緒池?

我們知道不用執行緒池的話,每個執行緒都要通過 new Thread(xxRunnable).start()的方式來建立並執行一個執行緒,執行緒少的話這不會是問題,而真實環境可能會開啟多個執行緒讓系統和程式達到最佳效率,當執行緒數達到一定數量就會耗盡系統的 CPU 和記憶體資源,也會造成 GC頻繁收集和停頓,因為每次建立和銷燬一個執行緒都是要消耗系統資源的,如果為每個任務都建立執行緒這無疑是一個很大的效能瓶頸。所以,執行緒池中的執行緒複用極大節省了系統資源,當執行緒一段時間不再有任務處理時它也會自動銷燬,而不會長駐記憶體。

阿里Java資深架構師詳解大廠多執行緒面試題,想進大廠這些你不得不知

16、常用的幾種執行緒池並講講其中的工作原理。

什麼是執行緒池?

很簡單,簡單看名字就知道是裝有執行緒的池子,我們可以把要執行的多執行緒交給執行緒池來處理,和連線池的概念一樣,通過維護一定數量的執行緒池來達到多個執行緒的複用。

執行緒池的好處

我們知道不用執行緒池的話,每個執行緒都要通過 new Thread(xxRunnable).start()的方式來建立並執行一個執行緒,執行緒少的話這不會是問題,而真實環境可能會開啟多個執行緒讓系統和程式達到最佳效率,當執行緒數達到一定數量就會耗盡系統的 CPU 和記憶體資源,也會造成 GC頻繁收集和停頓,因為每次建立和銷燬一個執行緒都是要消耗系統資源的,如果為每個任務都建立執行緒這無疑是一個很大的效能瓶頸。所以,執行緒池中的執行緒複用極大節省了系統資源,當執行緒一段時間不再有任務處理時它也會自動銷燬,而不會長駐記憶體。

執行緒池核心類

在 java.util.concurrent 包中我們能找到執行緒池的定義,其中 ThreadPoolExecutor 是我們執行緒池核心類,首先看看執行緒池類的主要引數有哪些。

如何提交執行緒

如可以先隨便定義一個固定大小的執行緒池ExecutorServicees=Executors.newFixedThreadPool(3);

提交一個執行緒es.submit(xxRunnble);

es.execute(xxRunnble);

submit 和 execute 分別有什麼區別呢?

execute 沒有返回值,如果不需要知道執行緒的結果就使用 execute 方法,效能會好很多。

submit 返回一個 Future 物件,如果想知道執行緒結果就使用 submit 提交,而且它能在主執行緒中通過 Future 的 get 方法捕獲執行緒中的異常。

如何關閉執行緒池es.shutdown();

不再接受新的任務,之前提交的任務等執行結束再關閉執行緒池。

es.shutdownNow();

不再接受新的任務,試圖停止池中的任務再關閉執行緒池,返回所有未處理的執行緒list 列表。

17、執行緒池啟動執行緒 submit()和 execute()方法有什麼不同?

execute 沒有返回值,如果不需要知道執行緒的結果就使用 execute 方法,效能會好很多。

submit 返回一個 Future 物件,如果想知道執行緒結果就使用 submit 提交,而且它能在主執行緒中通過 Future 的 get 方法捕獲執行緒中的異常。

18、CyclicBarrier 和 CountDownLatch 的區別?

兩個看上去有點像的類,都在 java.util.concurrent 下,都可以用來表示程式碼執行到某個點上,二者的區別在於:

1.CyclicBarrier 的某個執行緒執行到某個點上之後,該執行緒即停止執行,直到所有的執行緒都到達了這個點,所有執行緒才重新執行;CountDownLatch 則不是,某執行緒執行到某個點上之後,只是給某個數值-1 而已,該執行緒繼續執行

2.CyclicBarrier 只能喚起一個任務,CountDownLatch 可以喚起多個任務

3.CyclicBarrier 可重用,CountDownLatch 不可重用,計數值為 0 該 CountDownLatch就不可再用了

阿里Java資深架構師詳解大廠多執行緒面試題,想進大廠這些你不得不知

19、什麼是活鎖、飢餓、無鎖、死鎖?

死鎖、活鎖、飢餓是關於多執行緒是否活躍出現的執行阻塞障礙問題,如果執行緒出現了這三種情況,即執行緒不再活躍,不能再正常地執行下去了。

死鎖

死鎖是多執行緒中最差的一種情況,多個執行緒相互佔用對方的資源的鎖,而又相互等對方釋放鎖,此時若無外力干預,這些執行緒則一直處理阻塞的假死狀態,形成死鎖。

舉個例子,A 同學搶了 B 同學的鋼筆,B 同學搶了 A 同學的書,兩個人都相互佔用對方的東西,都在讓對方先還給自己自己再還,這樣一直爭執下去等待對方還而又得不到解決,老師知道此事後就讓他們相互還給對方,這樣在外力的干預下他們才解決,當然這只是個例子沒有老師他們也能很好解決,計算機不像人如果發現這種情況沒有外力干預還是會一直阻塞下去的。

活鎖

活鎖這個概念大家應該很少有人聽說或理解它的概念,而在多執行緒中這確實存在。活鎖恰恰與死鎖相反,死鎖是大家都拿不到資源都佔用著對方的資源,而活鎖是拿到資源卻又相互釋放不執行。當多執行緒中出現了相互謙讓,都主動將資源釋放給別的執行緒使用,這樣這個資源在多個執行緒之間跳動而又得不到執行,這就是活鎖。

飢餓

我們知道多執行緒執行中有執行緒優先順序這個東西,優先順序高的執行緒能夠插隊並優先執行,這樣如果優先順序高的執行緒一直搶佔優先順序低執行緒的資源,導致低優先順序執行緒無法得到執行,這就是飢餓。當然還有一種飢餓的情況,一個執行緒一直佔著一個資源不放而導致其他執行緒得不到執行,與死鎖不同的是飢餓在以後一段時間內還是能夠得到執行的,如那個佔用資源的執行緒結束了並釋放了資源。

無鎖

無鎖,即沒有對資源進行鎖定,即所有的執行緒都能訪問並修改同一個資源,但同時只有一個執行緒能修改成功。無鎖典型的特點就是一個修改操作在一個迴圈內進行,執行緒會不斷的嘗試修改共享資源,如果沒有衝突就修改成功並退出否則就會繼續下一次迴圈嘗試。所以,如果有多個執行緒修改同一個值必定會有一個執行緒能修改成功,而其他修改失敗的執行緒會不斷重試直到修改成功。之前的文章我介紹過 JDK 的 CAS 原理及應用即是無鎖的實現。

可以看出,無鎖是一種非常良好的設計,它不會出現執行緒出現的跳躍性問題,鎖使用不當肯定會出現系統效能問題,雖然無鎖無法全面代替有鎖,但無鎖在某些場合下是非常高效的。

20、什麼是原子性、可見性、有序性?

原子性、可見性、有序性是多執行緒程式設計中最重要的幾個知識點,由於多執行緒情況複雜,如何讓每個執行緒能看到正確的結果,這是非常重要的。

原子性

原子性是指一個執行緒的操作是不能被其他執行緒打斷,同一時間只有一個執行緒對一個變數進行操作。在多執行緒情況下,每個執行緒的執行結果不受其他執行緒的干擾,比如說多個執行緒同時對同一個共享成員變數 n++100 次,如果 n 初始值為 0,n 最後的值應該是 100,所以說它們是互不干擾的,這就是傳說的中的原子性。但 n++並不是原子性的操作,要使用 AtomicInteger 保證原子性。

可見性

可見性是指某個執行緒修改了某一個共享變數的值,而其他執行緒是否可以看見該共享變數修改後的值。在單執行緒中肯定不會有這種問題,單執行緒讀到的肯定都是最新的值,而在多執行緒程式設計中就不一定了。每個執行緒都有自己的工作記憶體,執行緒先把共享變數的值從主記憶體讀到工作記憶體,形成一個副本,當計算完後再把副本的值刷回主記憶體,從讀取到最後刷回主記憶體這是一個過程,當還沒刷回主記憶體的時候這時候對其他執行緒是不可見的,所以其他執行緒從主記憶體讀到的值是修改之前的舊值。像 CPU 的快取優化、硬體優化、指令重排及對 JVM 編譯器的優化,都會出現可見性的問題。

有序性

我們都知道程式是按程式碼順序執行的,對於單執行緒來說確實是如此,但在多執行緒情況下就不是如此了。為了優化程式執行和提高 CPU 的處理效能,JVM 和作業系統都會對指令進行重排,也就說前面的程式碼並不一定都會在後面的程式碼前面執行,即後面的程式碼可能會插到前面的程式碼之前執行,只要不影響當前執行緒的執行結果。所以,指令重排只會保證當前執行緒執行結果一致,但指令重排後勢必會影響多執行緒的執行結果。雖然重排序優化了效能,但也是會遵守一些規則的,並不能隨便亂排序,只是重排序會影響多執行緒執行的結果。

阿里Java資深架構師詳解大廠多執行緒面試題,想進大廠這些你不得不知

由於篇幅過長,就不一一發出答案了,如果有需要的話可以參考結尾的獲取方式。

阿里Java資深架構師詳解大廠多執行緒面試題,想進大廠這些你不得不知

當然進大廠這些掌握還不夠,還需要掌握:

阿里Java資深架構師詳解大廠多執行緒面試題,想進大廠這些你不得不知
阿里Java資深架構師詳解大廠多執行緒面試題,想進大廠這些你不得不知
阿里Java資深架構師詳解大廠多執行緒面試題,想進大廠這些你不得不知

對於架構技術方向我們有自己的高清思維方向導圖以及阿里架構師講解的架構視訊分享(包括高可用,高併發,spring原始碼,mybatis原始碼,JVM,大資料,Netty等多個技術知識的架構視訊資料和各種電子書籍閱讀)視訊資料獲取方式關注我後戳這裡加入免費獲取

精講架構視訊資料獲取方式關注我後戳這裡加入免費獲取

以及一些一線網際網路公司的面試題解析含答案


相關文章