1. 程式人生 > >Java多執行緒與併發基礎面試題

Java多執行緒與併發基礎面試題

> CS-LogN思維導圖:記錄專業基礎 面試題 開源地址:https://github.com/FISHers6/CS-LogN ![](https://img2020.cnblogs.com/blog/1454456/202006/1454456-20200619205756557-1128144157.png) # 多執行緒與併發基礎 ## 實現多執行緒 ### 面試題1:有幾種實現執行緒的方法,分別是什麼 - 1.繼承Thread類,啟動執行緒的唯一方法就是通過 Thread 類的 start()例項方法,start()方法是一個 native 方法,它將啟動一個新執行緒去執行 run()方法 - 2.實現 Runnable 介面,重寫run()函式,作為引數放到Thread類建構函式中作為target屬性,執行start()方法 - 執行緒池建立執行緒、Callable本質還是使Runnable建立,Callable是父輩類繼承了Runnable,執行緒池需傳入引數 - ### 面試題2:實現Runnable方法好,還是繼承Thread類好 - 實現Runnable介面更好 - 1.單一繼承原則,如果繼承了Thread,就不能繼承其它類了,限制了可擴充套件性 - 2.Thread類每次只能建立一個獨立的執行緒,損耗大,而Runnable能利用執行緒池工具來建立執行緒 - 3.從程式碼架構上看,run內容應該與Trhead程式碼解耦 ### 面試題3:一個執行緒兩次呼叫start方法會出現什麼情況(考察原始碼) - 第二次會出現異常,從start原始碼上和執行緒生命週期上分析,一個執行緒start後, 改變了threadState狀態字;而第二次再start每次會先檢查這個狀態不是0就報異常 ### 面試題4:既然start方法會呼叫run方法,為什麼我們還是要用start方法,而不是直接呼叫run方法呢(考察原始碼) - 因為start後執行緒才會經過完整的執行緒生命週期,start呼叫native start0,虛擬機器執startThread,thread_entry入口中呼叫Thread的run, ### 面試題5:start和run有什麼區別 - run()方法:只是普通的方法,呼叫run普通方法,可以重複多次呼叫 - start()方法,會啟動一個執行緒,使得虛擬機器去呼叫Runnable物件的run()方法,不能多次啟動同一個執行緒 ### 面試題6:start方法如何呼叫run方法的(考察原始碼和JVM) - start方法呼叫native start0,JVM虛擬機器執行startThread,在thread_entry中呼叫Thread的run方法 ### 面試題7:如何正確停止執行緒 - 使用interrupt中斷通知,而不是強制,中斷通知後會讓被停止執行緒去決定何時停止,即把主動權交給需要被中斷的執行緒 ## 執行緒的生命週期 ### 面試題1:Java執行緒有哪幾種狀態 說說生命週期 - 六種生命狀態(若time_waiting也算一種) - New,已建立但還尚未啟動的新執行緒 - Runable,可執行狀態;對應作業系統的兩種狀態“就緒態” 和 “執行態”(分配到CPU) - Blocked,阻塞狀態;請求synchronized鎖未分配到時阻塞,直到獲取到monitor鎖再進入Runnable - Waiting,等待狀態 - Timed waiting,限期等待 - Terminated終止狀態 - 執行緒的生命週期 狀態轉換圖 - ## Thread和Object類中 與執行緒相關的重要方法 ### 面試題1:實現兩個執行緒交替列印奇數偶數 ### 面試題2:手寫生產者消費者設計模式,為什麼用該模式 - 主要是為了解耦,匹配不同的能力 ### 面試題3:wait後發生了什麼,為什麼需要在同步程式碼內才能使用 - 從jvm的原始碼實現上看,wait後,執行緒讓出佔有的cpu並釋放同步資源鎖;把自己加入到等待池,以後不會再主動參與cpu的競爭,除非被其它notify命中 - 為了確保執行緒安全;另外wait會釋放資源,所以肯定要先拿到這個鎖,能進入同步程式碼塊已經拿到了鎖 ### 面試題4:為什麼執行緒通訊的方法wait,notify和notifyAll放在Object類,而sleep定義在Thread類裡 (考察物件鎖) - 與物件的鎖有關,物件鎖繫結在物件的物件頭中,且放在Object裡,使每個執行緒都可以持有多個物件的鎖 ### 面試題5:wait方法是屬於Object物件的,那呼叫Thread.wait會怎麼樣 - 執行緒死的時候會自己notifyAll,釋放掉所有的持有自己物件的鎖。這個機制是實現很多同步方法的基礎。如果呼叫Thrad.wait,干擾了我們設計的同步業務流程 ### 面試題6:如何選擇notify還是notifyAll - 優先選用notifyAll,喚醒所有執行緒;除非業務需要每次只喚醒一個執行緒的 ### 面試題7:notfiy後發生的操作,notifyAll之後所有的執行緒都會再次搶奪鎖,如果某執行緒搶奪失敗怎麼辦? - notify後,讓waiterSet等待池中的一個執行緒與entry_List鎖池一級活躍執行緒一起競爭CPU - 搶奪鎖失敗後會繼續待在原鎖池或原等待池,等待競爭CPU的排程 ### 面試題8:sleep方法與notify/wait方法的異同點 - 相同點:執行緒都會進入waiting狀態,都可以響應中斷 - 不同點:1.所屬類不同;2.wait/notify必須用在同步方法中,且會釋放鎖;3.sleep可以指定時間 ### 面試題9:join方法後父執行緒進入什麼狀態 - waiting狀態,join內部呼叫wait,子執行緒結束後自動呼叫notifyAll喚醒(jvm:exit函式) ## 執行緒安全與效能 ### 面試題1:守護執行緒和普通執行緒的區別 - 守護執行緒是服務於普通執行緒的,並且不會影響到jvm的退出 ### 面試題2:什麼是執行緒安全 - 不管業務中遇到怎樣的多個執行緒訪問某物件或某方法的情況,而在程式設計這個業務邏輯的時候,都不需要再額外做任何額外的處理(也就是可以像單執行緒程式設計一樣),程式也可以正常執行(不會因為多執行緒而出錯),就可以稱為執行緒安全 ### 面試題3:有哪些執行緒不安全的情況,什麼原因導致的 - 1.資料爭用、同時操作,如資料讀寫由於同時寫,非原子性操作導致執行結果錯誤,a++ - 2.存在競爭,順序不當,如死鎖、活鎖、飢餓 ### 面試題4:什麼是多執行緒的上下文切換,及導致的後果 - 程序執行緒切換要儲存所需要的CPU執行環境,如暫存器、棧、全域性變數等資源 - 在頻繁的io以及搶鎖的時候,會導致密集的上下文切換,多執行緒切換時,由於快取和上下文的切換會帶來效能問題 ### 面試題5:多執行緒導致的開銷有哪些 - 1.上下文切換開銷,如儲存快取(cache、快表等)的開銷 - 2.同步協作的開銷(java記憶體模型) - 為了資料的正確性,同步手段往往會使用禁止編譯器優化(如指令重排序優化、鎖粗化等),效能變差 - 使CPU內的快取失效(比如volatile可見性讓自己執行緒的快取失效後,必須使用主存來檢視資料) ## Java記憶體模型 ### 面試題1:Java的程式碼如何一步步轉化,最終被CPU執行的 - - 1. 最開始,我們編寫的Java程式碼,是*.java檔案 2. 在編譯(javac命令)後,從剛才的*.java檔案會變出一個新的Java位元組碼檔案*.class 3. JVM會執行剛才生成的位元組碼檔案(*.class),並把位元組碼檔案轉化為機器指令 4. 機器指令可以直接在CPU上執執行,也就是最終的程式執行 - JVM實現會帶來不同的“翻譯”,不同的CPU平臺的機器指令又千差萬別,無法保證併發安全的效果一致 ### 面試題2:單例模式的作用和適用場景 - 單例模式:只獲取一次資源,全程式通用,節省記憶體和計算;保證多執行緒計算結果正確;方便管理; 比如日期工具類只需要一個例項就可以,無需多個示例 ### 面試題3:單例模式的寫法,考察(重排序、單例和高併發的關係) - 餓漢式(靜態常量、靜態程式碼塊) - 原理1:static靜態常量在類載入的時候就初始化完成了,且由jvm保證執行緒安全,保證了變數唯一 - 原理2:靜態程式碼塊中例項化和靜態常量類似;放在靜態程式碼塊裡初始化,類載入時完成; - 特徵:簡單,但沒有懶載入(需要時再載入) - 懶漢式(加synchronized鎖) - 對初始化的方法加synchronized鎖達到執行緒安全的目的,但效率低,多執行緒下變成了同步 - 懶漢式取名:用到的時候才去載入 - 雙重檢查 - 程式碼實現 - 屬性加volatile,兩次if判斷NULL值,第二次前加類鎖 - 優點 - 執行緒安全;延遲載入;效率高 - 為什麼用雙重而不用單層 - 從執行緒安全方面、效率方面講 - 靜態內部類 - 需要理解靜態內部類的優點,懶漢式載入,jvm載入順序 - 列舉 - 程式碼實現簡單 - public enum Singleton{ INSTANCE; public void method(){} } - 保證了執行緒安全 - 列舉是一個特殊的類,經過反編譯檢視,列舉最終被編譯成一個final的類,繼承了列舉父類。各個例項通過static定義,本質就是一個靜態的物件,所有第一次使用的時候採取載入(懶載入) - 避免反序列化破壞單例 - 避免了:比如用反射就繞過了構造方法,反序列化出多個例項 ### 面試題4:單例模式各種寫法分別適用的場合 - 1.最好的方法是列舉,因列舉被編譯成final類,用static定義靜態物件,懶載入。既保證了執行緒安全又避免了反序列化破壞單例 - 2.如果程式一開始要載入的資源太多,考慮到啟動速度,就應該使用懶載入 - 3.如果是物件的建立需要配置檔案(一開始要載入其它資源),就不適合用餓漢式 ### 面試題5:餓漢式單例的缺點 - 沒有懶載入(初始化時全部加載出),初始化開銷大 ### 面試題6:懶漢式單例的缺點 - 雖然用到的時候才去載入,但是由於加鎖,效能低 ### 面試題7:單例模式的雙重檢查寫法為什麼要用double-check - 從程式碼實現出發,保證執行緒安全、延遲載入效率高 ### 面試題8:為什麼雙重檢查要用volatile - 1.保證instance的可見性 - 類初始化分成3條指令,重排序帶來NPE空虛指標問題,加volatile防止重排序 - 2.防止初始化指令重排序 ### 面試題9:講一講什麼是Java的記憶體模型 - 1.是一組規範,需要JVM實現遵守這個規範,以便實現安全的多執行緒程式 2.volatile、synchronized、Lock等同步工具和關鍵字實現原理都用到了JMM 3.重排序、記憶體可見性、原子性 ### 面試題10:什麼是happens-before,規則有哪些 - 解決可見性問題的:在時間上,動作A發生在動作B之前,B保證能看見A,這就是happens-before - 規則 - 1 單執行緒按程式碼順序規則;2 鎖操作(synchronized和Lock);3volatile變數;4.JUC工具類的Happens-Before原則 - 5.執行緒啟動時子執行緒啟動前能看到主執行緒run的所有內容;6.執行緒join主執行緒一定要等待子執行緒完成後再去做後面操作 - 7.傳遞性 8.中斷檢測 9.物件構造方法的最後一行指令 happens-before 於 finalize() 方法的第一行指令 ### 面試題11:講一講volatile關鍵字 - volatile是一種同步機制,比synchronized或者Lock相關類更輕量,因為使用volatile並不會發生上下文切換等開銷很大的行為。而加鎖時物件鎖會阻塞開銷大。 - 可見性,如果一個變數別修飾成volatile,那麼JVM就知道了這個變數可能會被併發修改; - 不能保證原子性 ### 面試題12:volatile的適用場合及作用 - 作用 - 1.保證可見性 2.禁止指令重排序(單例雙重鎖時) - 適合場景 - 適用場合1:boolean flag,布林具有原子性,可再由volatile保證其可見性 - 適用場合2:作為重新整理之前變數的觸發器 - 但不適合非原子性操作如:a++等 ### 面試題13:volatile和synchronized的異同 - 1 效能開銷方面: 鎖開銷更大,volatile無加鎖阻塞開銷 2 作用方面:volatile只能保證可見性,鎖既能保證可見性,又能保證原子性 ### 面試題14:什麼是記憶體可見性問題,為什麼存在 - 多執行緒下,一個執行緒修改共享資料後,其它執行緒能否感知到修改了資料的執行緒的變化 - CPU有多級快取,導致讀的資料過期,各處理機有獨自的快取未及時更新時,與主存內容不一致 ### 面試題15:主記憶體和本地記憶體的關係是什麼 - Java 作為高階語言,遮蔽了CPU cache等底層細節,用 JMM 定義了一套讀寫記憶體資料的規範,雖然我們不再需要關心一級快取和二級快取的問題,但是,JMM 抽象了主記憶體和本地記憶體的概念。 - 執行緒擁有自己的本地記憶體,並共享主記憶體的資料;執行緒讀寫共享資料也是通過本地記憶體交換的,所以才導致了可見性問題。 ### 面試題16:什麼是原子操作,Java的原子操作有哪些 - 原子操作 - 一系列的操作,要麼全部執行成功,要麼全部不執行,不會出現執行一半的情況,是不可分割的。 - 1)除long和double之外的基本型別(int, byte, boolean, short, char, float)的"賦值操作" - 2)所有"引用reference的賦值操作",不管是 32 位的機器還是 64 位的機器 - 3)java.concurrent.Atomic.* 包中所有類的原子操作 ### 面試題17:long 和 double 的原子性你瞭解嗎 - 在32位上的JVM上,long 和 double的操作不是原子的,但是在64位的JVM上是原子的。 - 在32位機器上一次只能讀寫32位;而浮點數、long型有8位元組64位;要分高32位和低32位兩條指令分開寫入,類似組合語言浮點數乘法分高低位暫存器;64位不用分兩次讀寫了 ### 面試題18:生成物件的過程是不是原子操作 - 不是,物件生成會生成分配空間、初始化、賦值,三條指令,有可能會被重排序,導致空指標 ### 面試題19:區分JVM記憶體結構、Java記憶體模型 、Java物件模型 - Java記憶體模型,和Java的併發程式設計有關 - 詳見面試題9 - JVM記憶體結構,和Java虛擬機器的執行時區域(堆疊)有關 - 堆區、方法區(存放常量池 引用 類資訊) 棧區、本地方法棧、程式計數器 - - Java物件模型,和Java物件在虛擬機器中的表現形式有關 - 是Java物件自身的儲存模型,在方法區中Kclass類資訊(虛擬函式表),在堆中存放new例項,線上程棧中存放引用,OOP-Klass Model ### 面試題20:什麼是重排序 - 指令實際執行順序和程式碼在java檔案中的順序不一致 - 重排序的好處:提高處理速度,包括編譯器優化、指令重排序(區域性性原理) ## 死鎖 ### 面試題1:寫一個必然死鎖的例子 - synchronized巢狀,構成請求迴圈 ### 面試題2:生產中什麼場景下會發生死鎖 - 併發中多執行緒互不相讓:當兩個(或更多)執行緒(或程序)相互持有對方所需要的資源,又不主動釋放,導致所有人都無法繼續前進,導致程式陷入無盡的阻塞,這就是死鎖。 ### 面試題3:發生死鎖必須滿足哪些條件 - 1.互斥 - 2.請求和保持 - 3.不可剝奪 - 4.儲存迴圈等待鏈 ### 面試題4:如何用工具定位死鎖 - 1.jstack命令在程式發生死鎖後,進行堆疊分析出死鎖執行緒 - 2.ThreadMXbean 程式執行中發現死鎖,一旦發現死鎖可以讓使用者去打日誌 ### 面試題5:有哪些解決死鎖問題的策略 - 1.死鎖語法,不讓死鎖發生 - 破壞死鎖的四個條件之一;如:哲學家換手、轉賬換序 - 2.死鎖避免 - 銀行家演算法、系統安全序列 - 3.死鎖檢查與恢復 - 適用資源請求分配圖,一段時間內檢查死鎖,有死鎖就恢復策略,採用恢復策略; - 恢復方法:程序終止 、資源剝奪 - 4.鴕鳥策略(忽略死鎖) - 先忽略,後期再讓人工恢復 ### 面試題6:死鎖避免策略和檢測與恢復策略的主要思路是什麼 - 死鎖語法 - 破壞死鎖的四大條件之一 - 死鎖避免 - 找到安全序列,銀行家演算法 - 死鎖檢測與恢復 - 資源請求分配圖 ### 面試題7:講一講經典的哲學家就餐問題,如何解決死鎖 - 什麼時候死鎖 - 哲學家各拿起自己左手邊的筷子,又去請求拿右手邊筷子迴圈請求時而阻塞 - 如何解決死鎖 - 1.一次兩隻筷子,形成原子性操作 - 2.只允許4個人拿有筷子 ### 面試題8:實際開發中如何避免死鎖 - 設定超時時間 - 多使用併發類而不是自己設計鎖 - 儘量降低鎖的使用粒度:用不同的鎖而不是一個鎖,鎖的範圍越小越好 - 避免鎖的巢狀:MustDeadLock類 - 分配資源前先看能不能收回來:銀行家演算法 - 儘量不要幾個功能用同一把鎖:專鎖專用 - 給你的執行緒起個有意義的名字:debug和排查時事半功倍,框架和JDK都遵守這個最佳實踐 ### 面試題9:什麼是活躍性問題?活鎖、飢餓和死鎖有什麼區別 - 活鎖 - 雖然執行緒並沒有阻塞,也始終在執行(所以叫做“活”鎖,執行緒是“活”的),但是程式卻得不到進展,因為執行緒始終互相謙讓,重複做同樣的事 - 工程中的活鎖例項:訊息佇列,訊息如果處理失敗,就放在佇列開頭重試,沒阻塞程式無法繼續 - 如何解決活鎖問題 - 加入隨機因素,乙太網的指數退避演算法 - 飢餓 - 當執行緒需要某些資源(例如CPU),但是卻始終得不到,可能原因是飢餓執行緒的優先