聽說,有位Java程序員學完這59道多線程面試題之後,直接面上阿里

前言

多線程是指從軟件或者硬件上實現多個線程併發執行的技術。具有多線程能力的計算機因有硬件支持而能夠在同一時間執行多於一個線程,進而提升整體處理性能。具有這種能力的系統包括對稱多處理機、多核心處理器以及芯片級多處理或同時多線程處理器。

軟件多線程,即便處理器只能運行一個線程,操作系統也可以通過快速的在不同線程之間進行切換,由於時間間隔很小,來給用戶造成一種多個線程同時運行的假象。這樣的程序運行機制被稱為軟件多線程。

聽說,有位Java程序員學完這59道多線程面試題之後,直接面上阿里

多線程有什麼優勢?

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,分別建立程序模型,並通過多線程分別運行這幾個任務,那就簡單很多了。

聽說,有位Java程序員學完這59道多線程面試題之後,直接面上阿里

多線程59道面試題
1.線程和進程的區別是什麼?

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

2.Java 實現線程有哪幾種方式?

1)繼承 Thread 類實現多線程

2)實現 Runnable 接口方式實現多線程

3)使用 ExecutorService、Callable、Future 實現有返回結果的多線程

3.啟動線程方法 start()和 run()有什麼區別?

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

4.怎麼終止一個線程?如何優雅地終止線程?

stop 終止,不推薦。

5.一個線程的生命週期有哪幾種狀態?它們之間如何流轉的?

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方法會放棄這個對象的監視器

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頻繁收集和停頓,因為每次創建和銷燬一個線程都是要消耗系統資源的,如果為每個任務都創建線程這無疑是一個很大的性能瓶頸。所以,線程池中的線程複用極大節省了系統資源,當線程一段時間不再有任務處理時它也會自動銷燬,而不會長駐內存。

16、常用的幾種線程池並講講其中的工作原理。

什麼是線程池?

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

線程池的好處

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

線程池核心類

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

如何提交線程

如 可 以 先 隨 便 定 義 一 個 固 定 大 小 的 線 程 池 ExecutorService es =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就不可再用了。

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

死鎖、活鎖、飢餓是關於多線程是否活躍出現的運行阻塞障礙問題,如果線程出現

了這三種情況,即線程不再活躍,不能再正常地執行下去了。

死鎖

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

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

活鎖

活鎖這個概念大家應該很少有人聽說或理解它的概念,而在多線程中這確實存在。

活鎖恰恰與死鎖相反,死鎖是大家都拿不到資源都佔用著對方的資源,而活鎖是拿到資源卻又相互釋放不執行。當多線程中出現了相互謙讓,都主動將資源釋放給別的線程使用,這樣這個資源在多個線程之間跳動而又得不到執行,這就是活鎖。

飢餓

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

無鎖

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

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

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

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

原子性

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

可見性

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

有序性

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

聽說,有位Java程序員學完這59道多線程面試題之後,直接面上阿里

由於文章篇幅限制,小編在這裡只介紹了20道多線程的面試題,剩下的39道就不做過多的介紹啦,如果大家需要這份多線程面試題的話,可以轉發此文關注小編,私信小編“學習”來得到獲取方式吧~~


分享到:


相關文章: