<div class="iteye-blog-content-contain" style="font-size: 14px"></div>
不管你是新程序員還是老手,你一定在面試中遇到過有關線程的問題。Java語言一個重要的特點就是內置了對併發的支持,讓Java大受企業和程序員的歡迎。大多數待遇豐厚的Java開發職位都要求開發者精通多線程技術並且有豐富的Java程序開發、調試、優化經驗,所以線程相關的問題在面試中經常會被提到。
在典型的Java面試中, 面試官會從線程的基本概念問起, 如:爲什麼你需要使用線程, 如何創建線程,用什麼方式創建線程比較好(比如:繼承thread類還是調用Runnable接口),然後逐漸問到併發問題像在Java併發編程的過程中遇到了什麼挑戰,Java內存模型,JDK1.5引入了哪些更高階的併發工具,併發編程常用的設計模式,經典多線程問題如生產者消費者,哲學家就餐,讀寫器或者簡單的有界緩衝區問題。僅僅知道線程的基本概念是遠遠不夠的, 你必須知道如何處理死鎖,競態條件,內存衝突和線程安全等併發問題。掌握了這些技巧,你就可以輕鬆應對多線程和併發面試了。
許多Java程序員在面試前纔會去看面試題,這很正常。因爲收集面試題和練習很花時間,所以我從許多面試者那裏收集了Java多線程和併發相關的50個熱門問題。我只收集了比較新的面試題且沒有提供全部答案。想必聰明的你對這些問題早就心中有數了, 如果遇到不懂的問題,你可以用Google找到答案。若你實在找不到答案,可以在文章的評論中向我求助。你也可以在這找到一些答案Java線程問答Top 12。
50道Java線程面試題
下面是Java線程相關的熱門面試題,你可以用它來好好準備面試。
1) 什麼是線程?
線程是操作系統能夠進行運算調度的最小單位,它被包含在進程之中,是進程中的實際運作單位。程序員可以通過它進行多處理器編程,你可以使用多線程對運算密集型任務提速。比如,如果一個線程完成一個任務要100毫秒,那麼用十個線程完成改任務只需10毫秒。Java在語言層面對多線程提供了卓越的支持,它也是一個很好的賣點。欲瞭解更多詳細信息請點擊這裏。
2) 線程和進程有什麼區別?
線程是進程的子集,一個進程可以有很多線程,每條線程並行執行不同的任務。不同的進程使用不同的內存空間,而所有的線程共享一片相同的內存空間。別把它和棧內存搞混,每個線程都擁有單獨的棧內存用來存儲本地數據。更多詳細信息請點擊這裏。
3) 如何在Java中實現線程?
在語言層面有兩種方式。java.lang.Thread 類的實例就是一個線程但是它需要調用java.lang.Runnable接口來執行,由於線程類本身就是調用的Runnable接口所以你可以繼承java.lang.Thread 類或者直接調用Runnable接口來重寫run()方法實現線程。更多詳細信息請點擊這裏.
4) 用Runnable還是Thread?
這個問題是上題的後續,大家都知道我們可以通過繼承Thread類或者調用Runnable接口來實現線程,問題是,那個方法更好呢?什麼情況下使用它?這個問題很容易回答,如果你知道Java不支持類的多重繼承,但允許你調用多個接口。所以如果你要繼承其他類,當然是調用Runnable接口好了。更多詳細信息請點擊這裏。
6) Thread 類中的start() 和 run() 方法有什麼區別?
這個問題經常被問到,但還是能從此區分出面試者對Java線程模型的理解程度。start()方法被用來啓動新創建的線程,而且start()內部調用了run()方法,這和直接調用run()方法的效果不一樣。當你調用run()方法的時候,只會是在原來的線程中調用,沒有新的線程啓動,start()方法纔會啓動新線程。更多討論請點擊這裏
7) Java中Runnable和Callable有什麼不同?
Runnable和Callable都代表那些要在不同的線程中執行的任務。Runnable從JDK1.0開始就有了,Callable是在JDK1.5增加的。它們的主要區別是Callable的 call() 方法可以返回值和拋出異常,而Runnable的run()方法沒有這些功能。Callable可以返回裝載有計算結果的Future對象。我的博客有更詳細的說明。
8) Java中CyclicBarrier 和 CountDownLatch有什麼不同?
CyclicBarrier 和 CountDownLatch 都可以用來讓一組線程等待其它線程。與 CyclicBarrier 不同的是,CountdownLatch 不能重新使用。點此查看更多信息和示例代碼。
9) Java內存模型是什麼?
Java內存模型規定和指引Java程序在不同的內存架構、CPU和操作系統間有確定性地行爲。它在多線程的情況下尤其重要。Java內存模型對一個線程所做的變動能被其它線程可見提供了保證,它們之間是先行發生關係。這個關係定義了一些規則讓程序員在併發編程時思路更清晰。比如,先行發生關係確保了:
- 線程內的代碼能夠按先後順序執行,這被稱爲程序次序規則。
- 對於同一個鎖,一個解鎖操作一定要發生在時間上後發生的另一個鎖定操作之前,也叫做管程鎖定規則。
- 前一個對
volatile
的寫操作在後一個volatile
的讀操作之前,也叫volatile
變量規則。 - 一個線程內的任何操作必需在這個線程的start()調用之後,也叫作線程啓動規則。
- 一個線程的所有操作都會在線程終止之前,線程終止規則。
- 一個對象的終結操作必需在這個對象構造完成之後,也叫對象終結規則。
- 可傳遞性
我強烈建議大家閱讀《Java併發編程實踐》第十六章來加深對Java內存模型的理解。
10) Java中的volatile 變量是什麼?
volatile是一個特殊的修飾符,只有成員變量才能使用它。在Java併發程序缺少同步類的情況下,多線程對成員變量的操作對其它線程是透明的。volatile變量可以保證下一個讀取操作會在前一個寫操作之後發生,就是上一題的volatile變量規則。點擊這裏查看更多volatile的相關內容。
11) 什麼是線程安全?Vector是一個線程安全類嗎? (詳見這裏)
如果你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。如果每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。一個線程安全的計數器類的同一個實例對象在被多個線程使用的情況下也不會出現計算失誤。很顯然你可以將集合類分成兩組,線程安全和非線程安全的。Vector 是用同步方法來實現線程安全的, 而和它相似的ArrayList不是線程安全的。
12) Java中什麼是競態條件? 舉個例子說明。
競態條件會導致程序在併發情況下出現一些bugs。多線程對一些資源的競爭的時候就會產生競態條件,如果首先要執行的程序競爭失敗排到後面執行了,那麼整個程序就會出現一些不確定的bugs。這種bugs很難發現而且會重複出現,因爲線程間的隨機競爭。一個例子就是無序處理,詳見答案。
13) Java中如何停止一個線程?
Java提供了很豐富的API但沒有爲停止線程提供API。JDK 1.0本來有一些像stop(), suspend() 和 resume()的控制方法但是由於潛在的死鎖威脅因此在後續的JDK版本中他們被棄用了,之後Java API的設計者就沒有提供一個兼容且線程安全的方法來停止一個線程。當run() 或者 call() 方法執行完的時候線程會自動結束,如果要手動結束一個線程,你可以用volatile 布爾變量來退出run()方法的循環或者是取消任務來中斷線程。點擊這裏查看示例代碼。
14) 一個線程運行時發生異常會怎樣?
這是我在一次面試中遇到的一個很刁鑽的Java面試題, 簡單的說,如果異常沒有被捕獲該線程將會停止執行。Thread.UncaughtExceptionHandler是用於處理未捕獲異常造成線程突然中斷情況的一個內嵌接口。當一個未捕獲異常將造成線程中斷的時候JVM會使用Thread.getUncaughtExceptionHandler()來查詢線程的UncaughtExceptionHandler並將線程和異常作爲參數傳遞給handler的uncaughtException()方法進行處理。
15) 如何在兩個線程間共享數據?
你可以通過共享對象來實現這個目的,或者是使用像阻塞隊列這樣併發的數據結構。這篇教程《Java線程間通信》(涉及到在兩個線程間共享對象)用wait和notify方法實現了生產者消費者模型。
16) Java中notify 和 notifyAll有什麼區別?
這又是一個刁鑽的問題,因爲多線程可以等待單監控鎖,Java API 的設計人員提供了一些方法當等待條件改變的時候通知它們,但是這些方法沒有完全實現。notify()方法不能喚醒某個具體的線程,所以只有一個線程在等待的時候它纔有用武之地。而notifyAll()喚醒所有線程並允許他們爭奪鎖確保了至少有一個線程能繼續運行。我的博客有更詳細的資料和示例代碼。
17) 爲什麼wait, notify 和 notifyAll這些方法不在thread類裏面?
這是個設計相關的問題,它考察的是面試者對現有系統和一些普遍存在但看起來不合理的事物的看法。回答這些問題的時候,你要說明爲什麼把這些方法放在Object類裏是有意義的,還有不把它放在Thread類裏的原因。一個很明顯的原因是JAVA提供的鎖是對象級的而不是線程級的,每個對象都有鎖,通過線程獲得。如果線程需要等待某些鎖那麼調用對象中的wait()方法就有意義了。如果wait()方法定義在Thread類中,線程正在等待的是哪個鎖就不明顯了。簡單的說,由於wait,notify和notifyAll都是鎖級別的操作,所以把他們定義在Object類中因爲鎖屬於對象。你也可以查看這篇文章瞭解更多。
18) 什麼是ThreadLocal變量?
ThreadLocal是Java裏一種特殊的變量。每個線程都有一個ThreadLocal就是每個線程都擁有了自己獨立的一個變量,競爭條件被徹底消除了。它是爲創建代價高昂的對象獲取線程安全的好方法,比如你可以用ThreadLocal讓SimpleDateFormat變成線程安全的,因爲那個類創建代價高昂且每次調用都需要創建不同的實例所以不值得在局部範圍使用它,如果爲每個線程提供一個自己獨有的變量拷貝,將大大提高效率。首先,通過複用減少了代價高昂的對象的創建個數。其次,你在沒有使用高代價的同步或者不變性的情況下獲得了線程安全。線程局部變量的另一個不錯的例子是ThreadLocalRandom類,它在多線程環境中減少了創建代價高昂的Random對象的個數。查看答案瞭解更多。
19) 什麼是FutureTask?
在Java併發程序中FutureTask表示一個可以取消的異步運算。它有啓動和取消運算、查詢運算是否完成和取回運算結果等方法。只有當運算完成的時候結果才能取回,如果運算尚未完成get方法將會阻塞。一個FutureTask對象可以對調用了Callable和Runnable的對象進行包裝,由於FutureTask也是調用了Runnable接口所以它可以提交給Executor來執行。
20) Java中interrupted 和 isInterruptedd方法的區別?
interrupted() 和 isInterrupted()的主要區別是前者會將中斷狀態清除而後者不會。Java多線程的中斷機制是用內部標識來實現的,調用Thread.interrupt()來中斷一個線程就會設置中斷標識爲true。當中斷線程調用靜態方法Thread.interrupted()來檢查中斷狀態時,中斷狀態會被清零。而非靜態方法isInterrupted()用來查詢其它線程的中斷狀態且不會改變中斷狀態標識。簡單的說就是任何拋出InterruptedException異常的方法都會將中斷狀態清零。無論如何,一個線程的中斷狀態有有可能被其它線程調用中斷來改變。
21) 爲什麼wait和notify方法要在同步塊中調用?
主要是因爲Java API強制要求這樣做,如果你不這麼做,你的代碼會拋出IllegalMonitorStateException異常。還有一個原因是爲了避免wait和notify之間產生競態條件。
22) 爲什麼你應該在循環中檢查等待條件?
處於等待狀態的線程可能會收到錯誤警報和僞喚醒,如果不在循環中檢查等待條件,程序就會在沒有滿足結束條件的情況下退出。因此,當一個等待線程醒來時,不能認爲它原來的等待狀態仍然是有效的,在notify()方法調用之後和等待線程醒來之前這段時間它可能會改變。這就是在循環中使用wait()方法效果更好的原因,你可以在Eclipse中創建模板調用wait和notify試一試。如果你想了解更多關於這個問題的內容,我推薦你閱讀《Effective Java》這本書中的線程和同步章節。
23) Java中的同步集合與併發集合有什麼區別?
同步集合與併發集合都爲多線程和併發提供了合適的線程安全的集合,不過併發集合的可擴展性更高。在Java1.5之前程序員們只有同步集合來用且在多線程併發的時候會導致爭用,阻礙了系統的擴展性。Java5介紹了併發集合像ConcurrentHashMap,不僅提供線程安全還用鎖分離和內部分區等現代技術提高了可擴展性。更多內容詳見答案。
24) Java中堆和棧有什麼不同?
爲什麼把這個問題歸類在多線程和併發面試題裏?因爲棧是一塊和線程緊密相關的內存區域。每個線程都有自己的棧內存,用於存儲本地變量,方法參數和棧調用,一個線程中存儲的變量對其它線程是不可見的。而堆是所有線程共享的一片公用內存區域。對象都在堆裏創建,爲了提升效率線程會從堆中弄一個緩存到自己的棧,如果多個線程使用該變量就可能引發問題,這時volatile 變量就可以發揮作用了,它要求線程從主存中讀取變量的值。
更多內容詳見答案。
25) 什麼是線程池? 爲什麼要使用它?
創建線程要花費昂貴的資源和時間,如果任務來了才創建線程那麼響應時間會變長,而且一個進程能創建的線程數有限。爲了避免這些問題,在程序啓動的時候就創建若干線程來響應處理,它們被稱爲線程池,裏面的線程叫工作線程。從JDK1.5開始,Java API提供了Executor框架讓你可以創建不同的線程池。比如單線程池,每次處理一個任務;數目固定的線程池或者是緩存線程池(一個適合很多生存期短的任務的程序的可擴展線程池)。更多內容詳見這篇文章。
26) 如何寫代碼來解決生產者消費者問題?
在現實中你解決的許多線程問題都屬於生產者消費者模型,就是一個線程生產任務供其它線程進行消費,你必須知道怎麼進行線程間通信來解決這個問題。比較低級的辦法是用wait和notify來解決這個問題,比較讚的辦法是用Semaphore 或者 BlockingQueue來實現生產者消費者模型,這篇教程有實現它。
27) 如何避免死鎖?
Java多線程中的死鎖
死鎖是指兩個或兩個以上的進程在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。這是一個嚴重的問題,因爲死鎖會讓你的程序掛起無法完成任務,死鎖的發生必須滿足以下四個條件:
- 互斥條件:一個資源每次只能被一個進程使用。
- 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
- 不剝奪條件:進程已獲得的資源,在末使用完之前,不能強行剝奪。
- 循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關係。
避免死鎖最簡單的方法就是阻止循環等待條件,將系統中所有的資源設置標誌位、排序,規定所有的進程申請資源必須以一定的順序(升序或降序)做操作來避免死鎖。這篇教程有代碼示例和避免死鎖的討論細節。
28) Java中活鎖和死鎖有什麼區別?
這是上題的擴展,活鎖和死鎖類似,不同之處在於處於活鎖的線程或進程的狀態是不斷改變的,活鎖可以認爲是一種特殊的飢餓。一個現實的活鎖例子是兩個人在狹小的走廊碰到,兩個人都試着避讓對方好讓彼此通過,但是因爲避讓的方向都一樣導致最後誰都不能通過走廊。簡單的說就是,活鎖和死鎖的主要區別是前者進程的狀態可以改變但是卻不能繼續執行。
29) 怎麼檢測一個線程是否擁有鎖?
我一直不知道我們竟然可以檢測一個線程是否擁有鎖,直到我參加了一次電話面試。在java.lang.Thread中有一個方法叫holdsLock(),它返回true如果當且僅當當前線程擁有某個具體對象的鎖。你可以查看這篇文章瞭解更多。
30) 你如何在Java中獲取線程堆棧?
對於不同的操作系統,有多種方法來獲得Java進程的線程堆棧。當你獲取線程堆棧時,JVM會把所有線程的狀態存到日誌文件或者輸出到控制檯。在Windows你可以使用Ctrl + Break組合鍵來獲取線程堆棧,Linux下用kill -3命令。你也可以用jstack這個工具來獲取,它對線程id進行操作,你可以用jps這個工具找到id。
31) JVM中哪個參數是用來控制線程的棧堆棧小的
這個問題很簡單, -Xss參數用來控制線程的堆棧大小。你可以查看JVM配置列表來了解這個參數的更多信息。
32) Java中synchronized 和 ReentrantLock 有什麼不同?
Java在過去很長一段時間只能通過synchronized關鍵字來實現互斥,它有一些缺點。比如你不能擴展鎖之外的方法或者塊邊界,嘗試獲取鎖時不能中途取消等。Java 5 通過Lock接口提供了更復雜的控制來解決這些問題。 ReentrantLock 類實現了 Lock,它擁有與 synchronized 相同的併發性和內存語義且它還具有可擴展性。你可以查看這篇文章瞭解更多
33) 有三個線程T1,T2,T3,怎麼確保它們按順序執行?
在多線程中有多種方法讓線程按特定順序執行,你可以用線程類的join()方法在一個線程中啓動另一個線程,另外一個線程完成該線程繼續執行。爲了確保三個線程的順序你應該先啓動最後一個(T3調用T2,T2調用T1),這樣T1就會先完成而T3最後完成。你可以查看這篇文章瞭解更多。
34) Thread類中的yield方法有什麼作用?
Yield方法可以暫停當前正在執行的線程對象,讓其它有相同優先級的線程執行。它是一個靜態方法而且只保證當前線程放棄CPU佔用而不能保證使其它線程一定能佔用CPU,執行yield()的線程有可能在進入到暫停狀態後馬上又被執行。點擊這裏查看更多yield方法的相關內容。
35) Java中ConcurrentHashMap的併發度是什麼?
ConcurrentHashMap把實際map劃分成若干部分來實現它的可擴展性和線程安全。這種劃分是使用併發度獲得的,它是ConcurrentHashMap類構造函數的一個可選參數,默認值爲16,這樣在多線程情況下就能避免爭用。欲瞭解更多併發度和內部大小調整請閱讀我的文章How ConcurrentHashMap works in Java。
36) Java中Semaphore是什麼?
Java中的Semaphore是一種新的同步類,它是一個計數信號。從概念上講,從概念上講,信號量維護了一個許可集合。如有必要,在許可可用前會阻塞每一個 acquire(),然後再獲取該許可。每個 release()添加一個許可,從而可能釋放一個正在阻塞的獲取者。但是,不使用實際的許可對象,Semaphore只對可用許可的號碼進行計數,並採取相應的行動。信號量常常用於多線程的代碼中,比如數據庫連接池。更多詳細信息請點擊這裏。
37)如果你提交任務時,線程池隊列已滿。會時發會生什麼?
這個問題問得很狡猾,許多程序員會認爲該任務會阻塞直到線程池隊列有空位。事實上如果一個任務不能被調度執行那麼ThreadPoolExecutor’s submit()方法將會拋出一個RejectedExecutionException異常。
38) Java線程池中submit() 和 execute()方法有什麼區別?
兩個方法都可以向線程池提交任務,execute()方法的返回類型是void,它定義在Executor接口中, 而submit()方法可以返回持有計算結果的Future對象,它定義在ExecutorService接口中,它擴展了Executor接口,其它線程池類像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有這些方法。更多詳細信息請點擊這裏。
39) 什麼是阻塞式方法?
阻塞式方法是指程序會一直等待該方法完成期間不做其他事情,ServerSocket的accept()方法就是一直等待客戶端連接。這裏的阻塞是指調用結果返回之前,當前線程會被掛起,直到得到結果之後纔會返回。此外,還有異步和非阻塞式方法在任務完成前就返回。更多詳細信息請點擊這裏。
40) Swing是線程安全的嗎? 爲什麼?
你可以很肯定的給出回答,Swing不是線程安全的,但是你應該解釋這麼回答的原因即便面試官沒有問你爲什麼。當我們說swing不是線程安全的常常提到它的組件,這些組件不能在多線程中進行修改,所有對GUI組件的更新都要在AWT線程中完成,而Swing提供了同步和異步兩種回調方法來進行更新。點擊這裏查看更多swing和線程安全的相關內容。
41) Java中invokeAndWait 和 invokeLater有什麼區別?
這兩個方法是Swing API 提供給Java開發者用來從當前線程而不是事件派發線程更新GUI組件用的。InvokeAndWait()同步更新GUI組件,比如一個進度條,一旦進度更新了,進度條也要做出相應改變。如果進度被多個線程跟蹤,那麼就調用invokeAndWait()方法請求事件派發線程對組件進行相應更新。而invokeLater()方法是異步調用更新組件的。更多詳細信息請點擊這裏。
42) Swing API中那些方法是線程安全的?
這個問題又提到了swing和線程安全,雖然組件不是線程安全的但是有一些方法是可以被多線程安全調用的,比如repaint(), revalidate()。 JTextComponent的setText()方法和JTextArea的insert() 和 append() 方法也是線程安全的。
43) 如何在Java中創建Immutable對象?
這個問題看起來和多線程沒什麼關係, 但不變性有助於簡化已經很複雜的併發程序。Immutable對象可以在沒有同步的情況下共享,降低了對該對象進行併發訪問時的同步化開銷。可是Java沒有@Immutable這個註解符,要創建不可變類,要實現下面幾個步驟:通過構造方法初始化所有成員、對變量不要提供setter方法、將所有的成員聲明爲私有的,這樣就不允許直接訪問這些成員、在getter方法中,不要直接返回對象本身,而是克隆對象,並返回對象的拷貝。我的文章how to make an object Immutable in Java有詳細的教程,看完你可以充滿自信。
44) Java中的ReadWriteLock是什麼?
一般而言,讀寫鎖是用來提升併發程序性能的鎖分離技術的成果。Java中的ReadWriteLock是Java 5 中新增的一個接口,一個ReadWriteLock維護一對關聯的鎖,一個用於只讀操作一個用於寫。在沒有寫線程的情況下一個讀鎖可能會同時被多個讀線程持有。寫鎖是獨佔的,你可以使用JDK中的ReentrantReadWriteLock來實現這個規則,它最多支持65535個寫鎖和65535個讀鎖。
45) 多線程中的忙循環是什麼?
忙循環就是程序員用循環讓一個線程等待,不像傳統方法wait(), sleep() 或 yield() 它們都放棄了CPU控制,而忙循環不會放棄CPU,它就是在運行一個空循環。這麼做的目的是爲了保留CPU緩存,在多核系統中,一個等待線程醒來的時候可能會在另一個內核運行,這樣會重建緩存。爲了避免重建緩存和減少等待重建的時間就可以使用它了。你可以查看這篇文章獲得更多信息。
46)volatile 變量和 atomic 變量有什麼不同?
這是個有趣的問題。首先,volatile 變量和 atomic 變量看起來很像,但功能卻不一樣。Volatile變量可以確保先行關係,即寫操作會發生在後續的讀操作之前, 但它並不能保證原子性。例如用volatile修飾count變量那麼 count++ 操作就不是原子性的。而AtomicInteger類提供的atomic方法可以讓這種操作具有原子性如getAndIncrement()方法會原子性的進行增量操作把當前值加一,其它數據類型和引用變量也可以進行相似操作。
47) 如果同步塊內的線程拋出異常會發生什麼?
這個問題坑了很多Java程序員,若你能想到鎖是否釋放這條線索來回答還有點希望答對。無論你的同步塊是正常還是異常退出的,裏面的線程都會釋放鎖,所以對比鎖接口我更喜歡同步塊,因爲它不用我花費精力去釋放鎖,該功能可以在finally block裏釋放鎖實現。
48) 單例模式的雙檢鎖是什麼?
這個問題在Java面試中經常被問到,但是面試官對回答此問題的滿意度僅爲50%。一半的人寫不出雙檢鎖還有一半的人說不出它的隱患和Java1.5是如何對它修正的。它其實是一個用來創建線程安全的單例的老方法,當單例實例第一次被創建時它試圖用單個鎖進行性能優化,但是由於太過於複雜在JDK1.4中它是失敗的,我個人也不喜歡它。無論如何,即便你也不喜歡它但是還是要了解一下,因爲它經常被問到。你可以查看how double checked locking on Singleton works這篇文章獲得更多信息。
49) 如何在Java中創建線程安全的Singleton?
這是上面那個問題的後續,如果你不喜歡雙檢鎖而面試官問了創建Singleton類的替代方法,你可以利用JVM的類加載和靜態變量初始化特徵來創建Singleton實例,或者是利用枚舉類型來創建Singleton,我很喜歡用這種方法。你可以查看這篇文章獲得更多信息。
50) 寫出3條你遵循的多線程最佳實踐
這種問題我最喜歡了,我相信你在寫併發代碼來提升性能的時候也會遵循某些最佳實踐。以下三條最佳實踐我覺得大多數Java程序員都應該遵循:
- 給你的線程起個有意義的名字。
這樣可以方便找bug或追蹤。OrderProcessor, QuoteProcessor or TradeProcessor 這種名字比 Thread-1. Thread-2 and Thread-3 好多了,給線程起一個和它要完成的任務相關的名字,所有的主要框架甚至JDK都遵循這個最佳實踐。 - 避免鎖定和縮小同步的範圍
鎖花費的代價高昂且上下文切換更耗費時間空間,試試最低限度的使用同步和鎖,縮小臨界區。因此相對於同步方法我更喜歡同步塊,它給我擁有對鎖的絕對控制權。 - 多用同步類少用wait 和 notify
首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 這些同步類簡化了編碼操作,而用wait和notify很難實現對複雜控制流的控制。其次,這些類是由最好的企業編寫和維護在後續的JDK中它們還會不斷優化和完善,使用這些更高等級的同步工具你的程序可以不費吹灰之力獲得優化。 - 多用併發集合少用同步集合
這是另外一個容易遵循且受益巨大的最佳實踐,併發集合比同步集合的可擴展性更好,所以在併發編程時使用併發集合效果更好。如果下一次你需要用到map,你應該首先想到用ConcurrentHashMap。我的文章Java併發集合有更詳細的說明。
51) 如何強制啓動一個線程?
這個問題就像是如何強制進行Java垃圾回收,目前還沒有覺得方法,雖然你可以使用System.gc()來進行垃圾回收,但是不保證能成功。在Java裏面沒有辦法強制啓動一個線程,它是被線程調度器控制着且Java沒有公佈相關的API。
52) Java中的fork join框架是什麼?
fork join框架是JDK7中出現的一款高效的工具,Java開發人員可以通過它充分利用現代服務器上的多處理器。它是專門爲了那些可以遞歸劃分成許多子模塊設計的,目的是將所有可用的處理能力用來提升程序的性能。fork join框架一個巨大的優勢是它使用了工作竊取算法,可以完成更多任務的工作線程可以從其它線程中竊取任務來執行。你可以查看這篇文章獲得更多信息。
53) Java多線程中調用wait() 和 sleep()方法有什麼不同?
Java程序中wait 和 sleep都會造成某種形式的暫停,它們可以滿足不同的需要。wait()方法用於線程間通信,如果等待條件爲真且其它線程被喚醒時它會釋放鎖,而sleep()方法僅僅釋放CPU資源或者讓當前線程停止執行一段時間,但不會釋放鎖。你可以查看這篇文章獲得更多信息。
以上就是50道熱門Java多線程和併發面試題啦。我沒有分享所有題的答案但給未來的閱讀者提供了足夠的提示和線索來尋找答案。如果你真的找不到某題的答案,聯繫我吧,我會加上去的。這篇文章不僅可以用來準備面試,還能檢查你對多線程、併發、設計模式和競態條件、死鎖和線程安全等線程問題的理解。我打算把這篇文章的問題弄成所有Java多線程問題的大合集,但是沒有你的幫助恐怖是不能完成的,你也可以跟我分享其它任何問題,包括那些你被問到卻還沒有找到答案的問題。這篇文章對初學者或者是經驗豐富的Java開發人員都很有用,過兩三年甚至五六年你再讀它也會受益匪淺。它可以擴展初學者尤其有用因爲這個可以擴展他們的知識面,我會不斷更新這些題,大家可以在文章後面的評論中提問,分享和回答問題一起把這篇面試題完善。
你應該知道的JAVA面試題
經常面試一些候選人,整理了下我面試使用的題目,陸陸續續整理出來的題目很多,所以每次會抽一部分來問。答案會在後面的文章中逐漸發佈出來。
基礎題目
- Java線程的狀態
- 進程和線程的區別,進程間如何通訊,線程間如何通訊
- HashMap的數據結構是什麼?如何實現的。和HashTable,ConcurrentHashMap的區別
- Cookie和Session的區別
- 索引有什麼用?如何建索引?
- ArrayList是如何實現的,ArrayList和LinkedList的區別?ArrayList如何實現擴容。
- equals方法實現
- 面向對象
- 線程狀態,BLOCKED和WAITING有什麼區別
- JVM如何加載字節碼文件
- JVM GC,GC算法。
- 什麼情況會出現Full GC,什麼情況會出現yong GC。
- JVM內存模型
- Java運行時數據區
- 事務的實現原理
技術深度
- 有沒有看過JDK源碼,看過的類實現原理是什麼。
- HTTP協議
- TCP協議
- 一致性Hash算法
- JVM如何加載字節碼文件
- 類加載器如何卸載字節碼
- IO和NIO的區別,NIO優點
- Java線程池的實現原理,keepAliveTime等參數的作用。
- HTTP連接池實現原理
- 數據庫連接池實現原理
- 數據庫的實現原理
技術框架
- 看過哪些開源框架的源碼
- 爲什麼要用Redis,Redis有哪些優缺點?Redis如何實現擴容?
- Netty是如何使用線程池的,爲什麼這麼使用
- 爲什麼要使用Spring,Spring的優缺點有哪些
- Spring的IOC容器初始化流程
- Spring的IOC容器實現原理,爲什麼可以通過byName和ByType找到Bean
- Spring AOP實現原理
- 消息中間件是如何實現的,技術難點有哪些
系統架構
- 如何搭建一個高可用系統
- 哪些設計模式可以增加系統的可擴展性
- 介紹設計模式,如模板模式,命令模式,策略模式,適配器模式、橋接模式、裝飾模式,觀察者模式,狀態模式,訪問者模式。
- 抽象能力,怎麼提高研發效率。
- 什麼是高內聚低耦合,請舉例子如何實現
- 什麼情況用接口,什麼情況用消息
- 如果AB兩個系統互相依賴,如何解除依賴
- 如何寫一篇設計文檔,目錄是什麼
- 什麼場景應該拆分系統,什麼場景應該合併系統
- 系統和模塊的區別,分別在什麼場景下使用
分佈式系統
- 分佈式事務,兩階段提交。
- 如何實現分佈式鎖
- 如何實現分佈式Session
- 如何保證消息的一致性
- 負載均衡
- 正向代理(客戶端代理)和反向代理(服務器端代理)
- CDN實現原理
- 怎麼提升系統的QPS和吞吐量
實戰能力
- 有沒有處理過線上問題?出現內存泄露,CPU利用率標高,應用無響應時如何處理的。
- 開發中有沒有遇到什麼技術問題?如何解決的
- 如果有幾十億的白名單,每天白天需要高併發查詢,晚上需要更新一次,如何設計這個功能。
- 新浪微博是如何實現把微博推給訂閱者
- Google是如何在一秒內把搜索結果返回給用戶的。
- 12306網站的訂票系統如何實現,如何保證不會票不被超賣。
- 如何實現一個秒殺系統,保證只有幾位用戶能買到某件商品。
軟能力
- 如何學習一項新技術,比如如何學習Java的,重點學習什麼
- 有關注哪些新的技術
- 工作任務非常多非常雜時如何處理
- 項目出現延遲如何處理
- 和同事的設計思路不一樣怎麼處理
- 如何保證開發質量
- 職業規劃是什麼?短期,長期目標是什麼
- 團隊的規劃是什麼
- 能介紹下從工作到現在自己的成長在那裏
京東商城-平臺研發部面試題
2 CAS算法大量併發時會不會有問題?
3 多個線程訪問redis命令的時候怎麼解決併發問題?
4 redis怎麼自增?
5 aop有幾種代理模式?默認是哪一種?是不是cglib?你知道哪些代理方式?
6 除了nginx,你還知道什麼什麼反向代理服務器?nginx和lvs有什麼區別?
7 有多臺服務器(沒指明是MySQL或redis)主從複製,如果有一臺宕機,怎麼自動適配?如果用zookeeper怎麼配置節點(因爲我答的是zookeeper和心跳檢測)?
8 用一條命令關閉所有tomcat、java服務程序?
9 tomcat只有1g內存,要上傳2g文件,怎麼辦?
10 js中this和java的this有什麼區別?
11 兩個tomcat端口號相同,nginx中怎麼配置?
12 nginx有哪些代理測略?
13 a引用b、b引用c,c引用a,問a、b、c會不會被gc?
14 談談你對原型的理解?
15 一個線程有多個子線程,主線程要在子線程執行完之後才能執行,怎麼實現?
深入理解Java之線程池
我們使用線程的時候就去創建一個線程,這樣實現起來非常簡便,但是就會有一個問題:
如果併發的線程數量很多,並且每個線程都是執行一個時間很短的任務就結束了,這樣頻繁創建線程就會大大降低系統的效率,因爲頻繁創建線程和銷燬線程需要時間。
那麼有沒有一種辦法使得線程可以複用,就是執行完一個任務,並不被銷燬,而是可以繼續執行其他的任務?
在Java中可以通過線程池來達到這樣的效果。今天我們就來詳細講解一下Java的線程池,首先我們從最核心的ThreadPoolExecutor類中的方法講起,然後再講述它的實現原理,接着給出了它的使用示例,最後討論了一下如何合理配置線程池的大小。
以下是本文的目錄大綱:
一.Java中的ThreadPoolExecutor類
二.深入剖析線程池實現原理
三.使用示例
四.如何合理配置線程池的大小
若有不正之處請多多諒解,並歡迎批評指正。
請尊重作者勞動成果,轉載請標明原文鏈接:
http://www.cnblogs.com/dolphin0520/p/3932921.html
一.Java中的ThreadPoolExecutor類
java.uitl.concurrent.ThreadPoolExecutor類是線程池中最核心的一個類,因此如果要透徹地瞭解Java中的線程池,必須先了解這個類。下面我們來看一下ThreadPoolExecutor類的具體實現源碼。
在ThreadPoolExecutor類中提供了四個構造方法:
public class ThreadPoolExecutor extends AbstractExecutorService { ..... public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue); public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory); public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler); public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler); ... }
從上面的代碼可以得知,ThreadPoolExecutor繼承了AbstractExecutorService類,並提供了四個構造器,事實上,通過觀察每個構造器的源碼具體實現,發現前面三個構造器都是調用的第四個構造器進行的初始化工作。
下面解釋下一下構造器中各個參數的含義:
- corePoolSize:核心池的大小,這個參數跟後面講述的線程池的實現原理有非常大的關係。在創建了線程池後,默認情況下,線程池中並沒有任何線程,而是等待有任務到來才創建線程去執行任務,除非調用了prestartAllCoreThreads()或者prestartCoreThread()方法,從這2個方法的名字就可以看出,是預創建線程的意思,即在沒有任務到來之前就創建corePoolSize個線程或者一個線程。默認情況下,在創建了線程池後,線程池中的線程數爲0,當有任務來之後,就會創建一個線程去執行任務,當線程池中的線程數目達到corePoolSize後,就會把到達的任務放到緩存隊列當中;
- maximumPoolSize:線程池最大線程數,這個參數也是一個非常重要的參數,它表示在線程池中最多能創建多少個線程;
- keepAliveTime:表示線程沒有任務執行時最多保持多久時間會終止。默認情況下,只有當線程池中的線程數大於corePoolSize時,keepAliveTime纔會起作用,直到線程池中的線程數不大於corePoolSize,即當線程池中的線程數大於corePoolSize時,如果一個線程空閒的時間達到keepAliveTime,則會終止,直到線程池中的線程數不超過corePoolSize。但是如果調用了allowCoreThreadTimeOut(boolean)方法,在線程池中的線程數不大於corePoolSize時,keepAliveTime參數也會起作用,直到線程池中的線程數爲0;
- unit:參數keepAliveTime的時間單位,有7種取值,在TimeUnit類中有7種靜態屬性:
TimeUnit.DAYS; //天 TimeUnit.HOURS; //小時 TimeUnit.MINUTES; //分鐘 TimeUnit.SECONDS; //秒 TimeUnit.MILLISECONDS; //毫秒 TimeUnit.MICROSECONDS; //微妙 TimeUnit.NANOSECONDS; //納秒
- workQueue:一個阻塞隊列,用來存儲等待執行的任務,這個參數的選擇也很重要,會對線程池的運行過程產生重大影響,一般來說,這裏的阻塞隊列有以下幾種選擇:
ArrayBlockingQueue; LinkedBlockingQueue; SynchronousQueue;
ArrayBlockingQueue和PriorityBlockingQueue使用較少,一般使用LinkedBlockingQueue和Synchronous。線程池的排隊策略與BlockingQueue有關。
- threadFactory:線程工廠,主要用來創建線程;
- handler:表示當拒絕處理任務時的策略,有以下四種取值:
ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。 ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。 ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程) ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務
具體參數的配置與線程池的關係將在下一節講述。
從上面給出的ThreadPoolExecutor類的代碼可以知道,ThreadPoolExecutor繼承了AbstractExecutorService,我們來看一下AbstractExecutorService的實現:
public abstract class AbstractExecutorService implements ExecutorService { protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) { }; protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) { }; public Future<?> submit(Runnable task) {}; public <T> Future<T> submit(Runnable task, T result) { }; public <T> Future<T> submit(Callable<T> task) { }; private <T> T doInvokeAny(Collection<? extends Callable<T>> tasks, boolean timed, long nanos) throws InterruptedException, ExecutionException, TimeoutException { }; public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException { }; public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { }; public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException { }; public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException { }; }
AbstractExecutorService是一個抽象類,它實現了ExecutorService接口。
我們接着看ExecutorService接口的實現:
public interface ExecutorService extends Executor { void shutdown(); boolean isShutdown(); boolean isTerminated(); boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException; <T> Future<T> submit(Callable<T> task); <T> Future<T> submit(Runnable task, T result); Future<?> submit(Runnable task); <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException; <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException; <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException; <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; }
而ExecutorService又是繼承了Executor接口,我們看一下Executor接口的實現:
public interface Executor { void execute(Runnable command); }
到這裏,大家應該明白了ThreadPoolExecutor、AbstractExecutorService、ExecutorService和Executor幾個之間的關係了。
Executor是一個頂層接口,在它裏面只聲明瞭一個方法execute(Runnable),返回值爲void,參數爲Runnable類型,從字面意思可以理解,就是用來執行傳進去的任務的;
然後ExecutorService接口繼承了Executor接口,並聲明瞭一些方法:submit、invokeAll、invokeAny以及shutDown等;
抽象類AbstractExecutorService實現了ExecutorService接口,基本實現了ExecutorService中聲明的所有方法;
然後ThreadPoolExecutor繼承了類AbstractExecutorService。
在ThreadPoolExecutor類中有幾個非常重要的方法:
execute() submit() shutdown() shutdownNow()
execute()方法實際上是Executor中聲明的方法,在ThreadPoolExecutor進行了具體的實現,這個方法是ThreadPoolExecutor的核心方法,通過這個方法可以向線程池提交一個任務,交由線程池去執行。
submit()方法是在ExecutorService中聲明的方法,在AbstractExecutorService就已經有了具體的實現,在ThreadPoolExecutor中並沒有對其進行重寫,這個方法也是用來向線程池提交任務的,但是它和execute()方法不同,它能夠返回任務執行的結果,去看submit()方法的實現,會發現它實際上還是調用的execute()方法,只不過它利用了Future來獲取任務執行結果(Future相關內容將在下一篇講述)。
shutdown()和shutdownNow()是用來關閉線程池的。
還有很多其他的方法:
比如:getQueue() 、getPoolSize() 、getActiveCount()、getCompletedTaskCount()等獲取與線程池相關屬性的方法,有興趣的朋友可以自行查閱API。
二.深入剖析線程池實現原理
在上一節我們從宏觀上介紹了ThreadPoolExecutor,下面我們來深入解析一下線程池的具體實現原理,將從下面幾個方面講解:
1.線程池狀態
2.任務的執行
3.線程池中的線程初始化
4.任務緩存隊列及排隊策略
5.任務拒絕策略
6.線程池的關閉
7.線程池容量的動態調整
1.線程池狀態
在ThreadPoolExecutor中定義了一個volatile變量,另外定義了幾個static final變量表示線程池的各個狀態:
volatile int runState; static final int RUNNING = 0; static final int SHUTDOWN = 1; static final int STOP = 2; static final int TERMINATED = 3;
runState表示當前線程池的狀態,它是一個volatile變量用來保證線程之間的可見性;
下面的幾個static final變量表示runState可能的幾個取值。
當創建線程池後,初始時,線程池處於RUNNING狀態;
如果調用了shutdown()方法,則線程池處於SHUTDOWN狀態,此時線程池不能夠接受新的任務,它會等待所有任務執行完畢;
如果調用了shutdownNow()方法,則線程池處於STOP狀態,此時線程池不能接受新的任務,並且會去嘗試終止正在執行的任務;
當線程池處於SHUTDOWN或STOP狀態,並且所有工作線程已經銷燬,任務緩存隊列已經清空或執行結束後,線程池被設置爲TERMINATED狀態。
2.任務的執行
在瞭解將任務提交給線程池到任務執行完畢整個過程之前,我們先來看一下ThreadPoolExecutor類中其他的一些比較重要成員變量:
private final BlockingQueue<Runnable> workQueue; //任務緩存隊列,用來存放等待執行的任務 private final ReentrantLock mainLock = new ReentrantLock(); //線程池的主要狀態鎖,對線程池狀態(比如線程池大小 //、runState等)的改變都要使用這個鎖 private final HashSet<Worker> workers = new HashSet<Worker>(); //用來存放工作集 private volatile long keepAliveTime; //線程存活時間 private volatile boolean allowCoreThreadTimeOut; //是否允許爲核心線程設置存活時間 private volatile int corePoolSize; //核心池的大小(即線程池中的線程數目大於這個參數時,提交的任務會被放進任務緩存隊列) private volatile int maximumPoolSize; //線程池最大能容忍的線程數 private volatile int poolSize; //線程池中當前的線程數 private volatile RejectedExecutionHandler handler; //任務拒絕策略 private volatile ThreadFactory threadFactory; //線程工廠,用來創建線程 private int largestPoolSize; //用來記錄線程池中曾經出現過的最大線程數 private long completedTaskCount; //用來記錄已經執行完畢的任務個數
每個變量的作用都已經標明出來了,這裏要重點解釋一下corePoolSize、maximumPoolSize、largestPoolSize三個變量。
corePoolSize在很多地方被翻譯成核心池大小,其實我的理解這個就是線程池的大小。舉個簡單的例子:
假如有一個工廠,工廠裏面有10個工人,每個工人同時只能做一件任務。
因此只要當10個工人中有工人是空閒的,來了任務就分配給空閒的工人做;
當10個工人都有任務在做時,如果還來了任務,就把任務進行排隊等待;
如果說新任務數目增長的速度遠遠大於工人做任務的速度,那麼此時工廠主管可能會想補救措施,比如重新招4個臨時工人進來;
然後就將任務也分配給這4個臨時工人做;
如果說着14個工人做任務的速度還是不夠,此時工廠主管可能就要考慮不再接收新的任務或者拋棄前面的一些任務了。
當這14個工人當中有人空閒時,而新任務增長的速度又比較緩慢,工廠主管可能就考慮辭掉4個臨時工了,只保持原來的10個工人,畢竟請額外的工人是要花錢的。
這個例子中的corePoolSize就是10,而maximumPoolSize就是14(10+4)。
也就是說corePoolSize就是線程池大小,maximumPoolSize在我看來是線程池的一種補救措施,即任務量突然過大時的一種補救措施。
不過爲了方便理解,在本文後面還是將corePoolSize翻譯成核心池大小。
largestPoolSize只是一個用來起記錄作用的變量,用來記錄線程池中曾經有過的最大線程數目,跟線程池的容量沒有任何關係。
下面我們進入正題,看一下任務從提交到最終執行完畢經歷了哪些過程。
在ThreadPoolExecutor類中,最核心的任務提交方法是execute()方法,雖然通過submit也可以提交任務,但是實際上submit方法裏面最終調用的還是execute()方法,所以我們只需要研究execute()方法的實現原理即可:
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) { if (runState == RUNNING && workQueue.offer(command)) { if (runState != RUNNING || poolSize == 0) ensureQueuedTaskHandled(command); } else if (!addIfUnderMaximumPoolSize(command)) reject(command); // is shutdown or saturated } }
上面的代碼可能看起來不是那麼容易理解,下面我們一句一句解釋:
首先,判斷提交的任務command是否爲null,若是null,則拋出空指針異常;
接着是這句,這句要好好理解一下:
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command))
由於是或條件運算符,所以先計算前半部分的值,如果線程池中當前線程數不小於核心池大小,那麼就會直接進入下面的if語句塊了。
如果線程池中當前線程數小於核心池大小,則接着執行後半部分,也就是執行:
addIfUnderCorePoolSize(command)
如果執行完addIfUnderCorePoolSize這個方法返回false,則繼續執行下面的if語句塊,否則整個方法就直接執行完畢了。
如果執行完addIfUnderCorePoolSize這個方法返回false,然後接着判斷:
if (runState == RUNNING && workQueue.offer(command))
如果當前線程池處於RUNNING狀態,則將任務放入任務緩存隊列;如果當前線程池不處於RUNNING狀態或者任務放入緩存隊列失敗,則執行:
addIfUnderMaximumPoolSize(command)
如果執行addIfUnderMaximumPoolSize方法失敗,則執行reject()方法進行任務拒絕處理。
回到前面:
if (runState == RUNNING && workQueue.offer(command))
這句的執行,如果說當前線程池處於RUNNING狀態且將任務放入任務緩存隊列成功,則繼續進行判斷:
if (runState != RUNNING || poolSize == 0)
這句判斷是爲了防止在將此任務添加進任務緩存隊列的同時其他線程突然調用shutdown或者shutdownNow方法關閉了線程池的一種應急措施。如果是這樣就執行:
ensureQueuedTaskHandled(command)
進行應急處理,從名字可以看出是保證 添加到任務緩存隊列中的任務得到處理。
我們接着看2個關鍵方法的實現:addIfUnderCorePoolSize和addIfUnderMaximumPoolSize:
private boolean addIfUnderCorePoolSize(Runnable firstTask) { Thread t = null; final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { if (poolSize < corePoolSize && runState == RUNNING) t = addThread(firstTask); //創建線程去執行firstTask任務 } finally { mainLock.unlock(); } if (t == null) return false; t.start(); return true; }
這個是addIfUnderCorePoolSize方法的具體實現,從名字可以看出它的意圖就是當低於核心吃大小時執行的方法。下面看其具體實現,首先獲取到鎖,因爲這地方涉及到線程池狀態的變化,先通過if語句判斷當前線程池中的線程數目是否小於核心池大小,有朋友也許會有疑問:前面在execute()方法中不是已經判斷過了嗎,只有線程池當前線程數目小於核心池大小纔會執行addIfUnderCorePoolSize方法的,爲何這地方還要繼續判斷?原因很簡單,前面的判斷過程中並沒有加鎖,因此可能在execute方法判斷的時候poolSize小於corePoolSize,而判斷完之後,在其他線程中又向線程池提交了任務,就可能導致poolSize不小於corePoolSize了,所以需要在這個地方繼續判斷。然後接着判斷線程池的狀態是否爲RUNNING,原因也很簡單,因爲有可能在其他線程中調用了shutdown或者shutdownNow方法。然後就是執行
t = addThread(firstTask);
這個方法也非常關鍵,傳進去的參數爲提交的任務,返回值爲Thread類型。然後接着在下面判斷t是否爲空,爲空則表明創建線程失敗(即poolSize>=corePoolSize或者runState不等於RUNNING),否則調用t.start()方法啓動線程。
我們來看一下addThread方法的實現:
private Thread addThread(Runnable firstTask) { Worker w = new Worker(firstTask); Thread t = threadFactory.newThread(w); //創建一個線程,執行任務 if (t != null) { w.thread = t; //將創建的線程的引用賦值爲w的成員變量 workers.add(w); int nt = ++poolSize; //當前線程數加1 if (nt > largestPoolSize) largestPoolSize = nt; } return t; }
在addThread方法中,首先用提交的任務創建了一個Worker對象,然後調用線程工廠threadFactory創建了一個新的線程t,然後將線程t的引用賦值給了Worker對象的成員變量thread,接着通過workers.add(w)將Worker對象添加到工作集當中。
下面我們看一下Worker類的實現:
private final class Worker implements Runnable { private final ReentrantLock runLock = new ReentrantLock(); private Runnable firstTask; volatile long completedTasks; Thread thread; Worker(Runnable firstTask) { this.firstTask = firstTask; } boolean isActive() { return runLock.isLocked(); } void interruptIfIdle() { final ReentrantLock runLock = this.runLock; if (runLock.tryLock()) { try { if (thread != Thread.currentThread()) thread.interrupt(); } finally { runLock.unlock(); } } } void interruptNow() { thread.interrupt(); } private void runTask(Runnable task) { final ReentrantLock runLock = this.runLock; runLock.lock(); try { if (runState < STOP && Thread.interrupted() && runState >= STOP) boolean ran = false; beforeExecute(thread, task); //beforeExecute方法是ThreadPoolExecutor類的一個方法,沒有具體實現,用戶可以根據 //自己需要重載這個方法和後面的afterExecute方法來進行一些統計信息,比如某個任務的執行時間等 try { task.run(); ran = true; afterExecute(task, null); ++completedTasks; } catch (RuntimeException ex) { if (!ran) afterExecute(task, ex); throw ex; } } finally { runLock.unlock(); } } public void run() { try { Runnable task = firstTask; firstTask = null; while (task != null || (task = getTask()) != null) { runTask(task); task = null; } } finally { workerDone(this); //當任務隊列中沒有任務時,進行清理工作 } } }
它實際上實現了Runnable接口,因此上面的Thread t = threadFactory.newThread(w);效果跟下面這句的效果基本一樣:
Thread t = new Thread(w);
相當於傳進去了一個Runnable任務,在線程t中執行這個Runnable。
既然Worker實現了Runnable接口,那麼自然最核心的方法便是run()方法了:
public void run() { try { Runnable task = firstTask; firstTask = null; while (task != null || (task = getTask()) != null) { runTask(task); task = null; } } finally { workerDone(this); } }
從run方法的實現可以看出,它首先執行的是通過構造器傳進來的任務firstTask,在調用runTask()執行完firstTask之後,在while循環裏面不斷通過getTask()去取新的任務來執行,那麼去哪裏取呢?自然是從任務緩存隊列裏面去取,getTask是ThreadPoolExecutor類中的方法,並不是Worker類中的方法,下面是getTask方法的實現:
Runnable getTask() { for (;;) { try { int state = runState; if (state > SHUTDOWN) return null; Runnable r; if (state == SHUTDOWN) // Help drain queue r = workQueue.poll(); else if (poolSize > corePoolSize || allowCoreThreadTimeOut) //如果線程數大於核心池大小或者允許爲核心池線程設置空閒時間, //則通過poll取任務,若等待一定的時間取不到任務,則返回null r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS); else r = workQueue.take(); if (r != null) return r; if (workerCanExit()) { //如果沒取到任務,即r爲null,則判斷當前的worker是否可以退出 if (runState >= SHUTDOWN) // Wake up others interruptIdleWorkers(); //中斷處於空閒狀態的worker return null; } // Else retry } catch (InterruptedException ie) { // On interruption, re-check runState } } }
在getTask中,先判斷當前線程池狀態,如果runState大於SHUTDOWN(即爲STOP或者TERMINATED),則直接返回null。
如果runState爲SHUTDOWN或者RUNNING,則從任務緩存隊列取任務。
如果當前線程池的線程數大於核心池大小corePoolSize或者允許爲核心池中的線程設置空閒存活時間,則調用poll(time,timeUnit)來取任務,這個方法會等待一定的時間,如果取不到任務就返回null。
然後判斷取到的任務r是否爲null,爲null則通過調用workerCanExit()方法來判斷當前worker是否可以退出,我們看一下workerCanExit()的實現:
private boolean workerCanExit() { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); boolean canExit; //如果runState大於等於STOP,或者任務緩存隊列爲空了 //或者 允許爲核心池線程設置空閒存活時間並且線程池中的線程數目大於1 try { canExit = runState >= STOP || workQueue.isEmpty() || (allowCoreThreadTimeOut && poolSize > Math.max(1, corePoolSize)); } finally { mainLock.unlock(); } return canExit; }
也就是說如果線程池處於STOP狀態、或者任務隊列已爲空或者允許爲核心池線程設置空閒存活時間並且線程數大於1時,允許worker退出。如果允許worker退出,則調用interruptIdleWorkers()中斷處於空閒狀態的worker,我們看一下interruptIdleWorkers()的實現:
void interruptIdleWorkers() { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { for (Worker w : workers) //實際上調用的是worker的interruptIfIdle()方法 w.interruptIfIdle(); } finally { mainLock.unlock(); } }
從實現可以看出,它實際上調用的是worker的interruptIfIdle()方法,在worker的interruptIfIdle()方法中:
void interruptIfIdle() { final ReentrantLock runLock = this.runLock; if (runLock.tryLock()) { //注意這裏,是調用tryLock()來獲取鎖的,因爲如果當前worker正在執行任務,鎖已經被獲取了,是無法獲取到鎖的 //如果成功獲取了鎖,說明當前worker處於空閒狀態 try { if (thread != Thread.currentThread()) thread.interrupt(); } finally { runLock.unlock(); } } }
這裏有一個非常巧妙的設計方式,假如我們來設計線程池,可能會有一個任務分派線程,當發現有線程空閒時,就從任務緩存隊列中取一個任務交給空閒線程執行。但是在這裏,並沒有採用這樣的方式,因爲這樣會要額外地對任務分派線程進行管理,無形地會增加難度和複雜度,這裏直接讓執行完任務的線程去任務緩存隊列裏面取任務來執行。
我們再看addIfUnderMaximumPoolSize方法的實現,這個方法的實現思想和addIfUnderCorePoolSize方法的實現思想非常相似,唯一的區別在於addIfUnderMaximumPoolSize方法是在線程池中的線程數達到了核心池大小並且往任務隊列中添加任務失敗的情況下執行的:
private boolean addIfUnderMaximumPoolSize(Runnable firstTask) { Thread t = null; final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { if (poolSize < maximumPoolSize && runState == RUNNING) t = addThread(firstTask); } finally { mainLock.unlock(); } if (t == null) return false; t.start(); return true; }
看到沒有,其實它和addIfUnderCorePoolSize方法的實現基本一模一樣,只是if語句判斷條件中的poolSize < maximumPoolSize不同而已。
到這裏,大部分朋友應該對任務提交給線程池之後到被執行的整個過程有了一個基本的瞭解,下面總結一下:
1)首先,要清楚corePoolSize和maximumPoolSize的含義;
2)其次,要知道Worker是用來起到什麼作用的;
3)要知道任務提交給線程池之後的處理策略,這裏總結一下主要有4點:
- 如果當前線程池中的線程數目小於corePoolSize,則每來一個任務,就會創建一個線程去執行這個任務;
- 如果當前線程池中的線程數目>=corePoolSize,則每來一個任務,會嘗試將其添加到任務緩存隊列當中,若添加成功,則該任務會等待空閒線程將其取出去執行;若添加失敗(一般來說是任務緩存隊列已滿),則會嘗試創建新的線程去執行這個任務;
- 如果當前線程池中的線程數目達到maximumPoolSize,則會採取任務拒絕策略進行處理;
- 如果線程池中的線程數量大於 corePoolSize時,如果某線程空閒時間超過keepAliveTime,線程將被終止,直至線程池中的線程數目不大於corePoolSize;如果允許爲核心池中的線程設置存活時間,那麼核心池中的線程空閒時間超過keepAliveTime,線程也會被終止。
3.線程池中的線程初始化
默認情況下,創建線程池之後,線程池中是沒有線程的,需要提交任務之後纔會創建線程。
在實際中如果需要線程池創建之後立即創建線程,可以通過以下兩個方法辦到:
- prestartCoreThread():初始化一個核心線程;
- prestartAllCoreThreads():初始化所有核心線程
下面是這2個方法的實現:
public boolean prestartCoreThread() { return addIfUnderCorePoolSize(null); //注意傳進去的參數是null } public int prestartAllCoreThreads() { int n = 0; while (addIfUnderCorePoolSize(null))//注意傳進去的參數是null ++n; return n; }
注意上面傳進去的參數是null,根據第2小節的分析可知如果傳進去的參數爲null,則最後執行線程會阻塞在getTask方法中的
r = workQueue.take();
即等待任務隊列中有任務。
4.任務緩存隊列及排隊策略
在前面我們多次提到了任務緩存隊列,即workQueue,它用來存放等待執行的任務。
workQueue的類型爲BlockingQueue<Runnable>,通常可以取下面三種類型:
1)ArrayBlockingQueue:基於數組的先進先出隊列,此隊列創建時必須指定大小;
2)LinkedBlockingQueue:基於鏈表的先進先出隊列,如果創建時沒有指定此隊列大小,則默認爲Integer.MAX_VALUE;
3)synchronousQueue:這個隊列比較特殊,它不會保存提交的任務,而是將直接新建一個線程來執行新來的任務。
5.任務拒絕策略
當線程池的任務緩存隊列已滿並且線程池中的線程數目達到maximumPoolSize,如果還有任務到來就會採取任務拒絕策略,通常有以下四種策略:
ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。 ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。 ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程) ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務
6.線程池的關閉
ThreadPoolExecutor提供了兩個方法,用於線程池的關閉,分別是shutdown()和shutdownNow(),其中:
- shutdown():不會立即終止線程池,而是要等所有任務緩存隊列中的任務都執行完後才終止,但再也不會接受新的任務
- shutdownNow():立即終止線程池,並嘗試打斷正在執行的任務,並且清空任務緩存隊列,返回尚未執行的任務
7.線程池容量的動態調整
ThreadPoolExecutor提供了動態調整線程池容量大小的方法:setCorePoolSize()和setMaximumPoolSize(),
- setCorePoolSize:設置核心池大小
- setMaximumPoolSize:設置線程池最大能創建的線程數目大小
當上述參數從小變大時,ThreadPoolExecutor進行線程賦值,還可能立即創建新的線程來執行任務。
三.使用示例
前面我們討論了關於線程池的實現原理,這一節我們來看一下它的具體使用:
public class Test { public static void main(String[] args) { ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(5)); for(int i=0;i<15;i++){ MyTask myTask = new MyTask(i); executor.execute(myTask); System.out.println("線程池中線程數目:"+executor.getPoolSize()+",隊列中等待執行的任務數目:"+ executor.getQueue().size()+",已執行玩別的任務數目:"+executor.getCompletedTaskCount()); } executor.shutdown(); } } class MyTask implements Runnable { private int taskNum; public MyTask(int num) { this.taskNum = num; } @Override public void run() { System.out.println("正在執行task "+taskNum); try { Thread.currentThread().sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("task "+taskNum+"執行完畢"); } }
執行結果:
正在執行task 0 線程池中線程數目:1,隊列中等待執行的任務數目:0,已執行玩別的任務數目:0 線程池中線程數目:2,隊列中等待執行的任務數目:0,已執行玩別的任務數目:0 正在執行task 1 線程池中線程數目:3,隊列中等待執行的任務數目:0,已執行玩別的任務數目:0 正在執行task 2 線程池中線程數目:4,隊列中等待執行的任務數目:0,已執行玩別的任務數目:0 正在執行task 3 線程池中線程數目:5,隊列中等待執行的任務數目:0,已執行玩別的任務數目:0 正在執行task 4 線程池中線程數目:5,隊列中等待執行的任務數目:1,已執行玩別的任務數目:0 線程池中線程數目:5,隊列中等待執行的任務數目:2,已執行玩別的任務數目:0 線程池中線程數目:5,隊列中等待執行的任務數目:3,已執行玩別的任務數目:0 線程池中線程數目:5,隊列中等待執行的任務數目:4,已執行玩別的任務數目:0 線程池中線程數目:5,隊列中等待執行的任務數目:5,已執行玩別的任務數目:0 線程池中線程數目:6,隊列中等待執行的任務數目:5,已執行玩別的任務數目:0 正在執行task 10 線程池中線程數目:7,隊列中等待執行的任務數目:5,已執行玩別的任務數目:0 正在執行task 11 線程池中線程數目:8,隊列中等待執行的任務數目:5,已執行玩別的任務數目:0 正在執行task 12 線程池中線程數目:9,隊列中等待執行的任務數目:5,已執行玩別的任務數目:0 正在執行task 13 線程池中線程數目:10,隊列中等待執行的任務數目:5,已執行玩別的任務數目:0 正在執行task 14 task 3執行完畢 task 0執行完畢 task 2執行完畢 task 1執行完畢 正在執行task 8 正在執行task 7 正在執行task 6 正在執行task 5 task 4執行完畢 task 10執行完畢 task 11執行完畢 task 13執行完畢 task 12執行完畢 正在執行task 9 task 14執行完畢 task 8執行完畢 task 5執行完畢 task 7執行完畢 task 6執行完畢 task 9執行完畢
從執行結果可以看出,當線程池中線程的數目大於5時,便將任務放入任務緩存隊列裏面,當任務緩存隊列滿了之後,便創建新的線程。如果上面程序中,將for循環中改成執行20個任務,就會拋出任務拒絕異常了。
不過在java doc中,並不提倡我們直接使用ThreadPoolExecutor,而是使用Executors類中提供的幾個靜態方法來創建線程池:
Executors.newCachedThreadPool(); //創建一個緩衝池,緩衝池容量大小爲Integer.MAX_VALUE Executors.newSingleThreadExecutor(); //創建容量爲1的緩衝池 Executors.newFixedThreadPool(int); //創建固定容量大小的緩衝池
下面是這三個靜態方法的具體實現:
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); } public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
從它們的具體實現來看,它們實際上也是調用了ThreadPoolExecutor,只不過參數都已配置好了。
newFixedThreadPool創建的線程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue;
newSingleThreadExecutor將corePoolSize和maximumPoolSize都設置爲1,也使用的LinkedBlockingQueue;
newCachedThreadPool將corePoolSize設置爲0,將maximumPoolSize設置爲Integer.MAX_VALUE,使用的SynchronousQueue,也就是說來了任務就創建線程運行,當線程空閒超過60秒,就銷燬線程。
實際中,如果Executors提供的三個靜態方法能滿足要求,就儘量使用它提供的三個方法,因爲自己去手動配置ThreadPoolExecutor的參數有點麻煩,要根據實際任務的類型和數量來進行配置。
另外,如果ThreadPoolExecutor達不到要求,可以自己繼承ThreadPoolExecutor類進行重寫。
四.如何合理配置線程池的大小
本節來討論一個比較重要的話題:如何合理配置線程池大小,僅供參考。
一般需要根據任務的類型來配置線程池大小:
如果是CPU密集型任務,就需要儘量壓榨CPU,參考值可以設爲 NCPU+1
如果是IO密集型任務,參考值可以設置爲2*NCPU
當然,這只是一個參考值,具體的設置還需要根據實際情況進行調整,比如可以先將線程池大小設置爲參考值,再觀察任務運行情況和系統負載、資源利用率來進行適當調整。
參考資料:
http://ifeve.com/java-threadpool/
http://blog.163.com/among_1985/blog/static/275005232012618849266/
http://developer.51cto.com/art/201203/321885.htm
http://blog.csdn.net/java2000_wl/article/details/22097059
http://blog.csdn.net/cutesource/article/details/6061229
http://blog.csdn.net/xieyuooo/article/details/8718741
《JDK API 1.6》
一篇文章搞定面試中的鏈表題目
廢話少說,上鍊表的數據結構
class ListNode {
ListNode next;
int val;
ListNode(int x){
val = x;
next = null;
}
}
1.翻轉鏈表
ListNode reverse(ListNode node){
ListNode prev = null;
while(node!=null){
ListNode tmp = node.next;
node.next = prev;
prev = node;
node = tmp;
}
return prev;
}
//翻轉鏈表(遞歸方式)
ListNode reverse2(ListNode head){
if(head.next == null){
return head;
}
ListNode reverseNode = reverse2(head.next);
head.next.next = head;
head.next = null;
return reverseNode;
}
2.判斷鏈表是否有環
boolean hasCycle(ListNode head){
if(head == null|| head.next == null){
return false;
}
ListNode slow,fast;
fast = head.next;
slow = head;
while(fast!=slow){
if(fast==null||fast.next==null){
return false;
}
fast = fast.next.next;
slow = slow.next;
}
return true;
}
3,鏈表排序
ListNode sortList(ListNode head){
if(head == null|| head.next == null){
return head;
}
ListNode mid = middleNode(head);
ListNode right = sortList(mid.next);
mid.next = null;
ListNode left = sortList(head);
return merge(left, right);
}
ListNode middleNode(ListNode head){
ListNode slow = head;
ListNode fast = head.next;
while(fast!=null&fast.next!=null){
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
ListNode merge(ListNode n1,ListNode n2){
ListNode dummy = new ListNode(0);
ListNode node = dummy;
while (n1!=null&&n2!=null) {
if(n1.val<n2.val){
node.next = n1;
n1 = n1.next;
}else{
node.next = n2;
n2 = n2.next;
}
node = node.next;
}
if(n1!=null){
node.next = n1;
}else{
node.next = n2;
}
return dummy.next;
}
4.鏈表相加求和
ListNode addLists(ListNode l1,ListNode l2){
if(l1==null&&l2==null){
return null;
}
ListNode head = new ListNode();
ListNode point = head;
int carry = 0;
while(l1!=null&&l2!=null){
int sum = carry + l1.val + l2.val;
point.next = new ListNode(sum%10);
carry = sum/10;
l1 = l1.next;
l2 = l2.next;
point = point.next;
}
while(l1!=null){
int sum = carry + l1.val;
point.next = new ListNode(sum%10);
carry = sum/10;
l1 = l1.next;
point = point.next;
}
while(l2!=null){
int sum = carry + l2.val;
point.next = new ListNode(sum%10);
carry = sum/10;
l2 = l2.next;
point = point.next;
}
if(carry!=0){
point.next = new ListNode(carry);
}
return head.next;
}
5.得到鏈表倒數第n個節點
ListNode nthToLast(ListNode head,int n ){
if(head == null||n<1){
return null;
}
ListNode l1 = head;
ListNode l2 = head;
for(int i = 0;i<n-1;i++){
if(l2 == null){
return null;
}
l2 = l2.next;
}
while(l2.next!=null){
l1 = l1.next;
l2 = l2.next;
}
return l1;
}
6.刪除鏈表倒數第n個節點
ListNode deletenthNode(ListNode head,int n){
// write your code here
if (n <= 0) {
return null;
}
ListNode dumy = new ListNode(0);
dumy.next = head;
ListNode prdDel = dumy;
for(int i = 0;i<n;i++){
if(head==null){
return null;
}
head = head.next;
}
while(head!=null){
head = head.next;
prdDel = prdDel.next;
}
prdDel.next = prdDel.next.next;
return dumy.next;
}
7.刪除鏈表中重複的元素
ListNode deleteMuNode(ListNode head){
if(head == null){
return null;
}
ListNode node = head;
while(node.next != null){
if(node.val == node.next.val){
node.next = node.next.next;
}else{
node = node.next;
}
}
return head;
}
8.刪除鏈表中重複的元素ii,去掉重複的節點
ListNode deleteMuNode2(ListNode head){
if(head == null||head.next == null){
return head;
}
ListNode dummy = new ListNode(0);
dummy.next = head;
head = dummy;
while(head.next!=null&&head.next.next!=null){
if(head.next.val == head.next.next.val){
int val = head.next.val;
while(head.next.val == val&&head.next != null){
head.next = head.next.next;
}
}else{
head = head.next;
}
}
return dummy.next;
}
9.旋轉鏈表
ListNode rotateRight(ListNode head,int k){
if(head ==null){
return null;
}
int length = getLength(head);
k = k % length;
ListNode dummy = new ListNode(0);
dummy.next = head;
head = dummy;
ListNode tail = dummy;
for(int i = 0;i<k;i++){
head = head.next;
}
while(head.next!= null){
head = head.next;
tail = tail.next;
}
head.next = dummy.next;
dummy.next = tail.next;
tail.next = null;
return dummy.next;
}
10.重排鏈表
ListNode reOrder(ListNode head){
if(head == null||head.next == null){
return;
}
ListNode mid = middleNode(head);
ListNode tail = reverse(mid.next);
mergeIndex(head, tail);
}
private void mergeIndex(ListNode head1,ListNode head2){
int index = 0;
ListNode dummy = new ListNode(0);
while (head1!=null&&head2!=null) {
if(index%2==0){
dummy.next = head1;
head1 = head1.next;
}else{
dummy.next = head2;
head2 = head2.next;
}
dummy = dummy.next;
index ++;
}
if(head1!=null){
dummy.next = head1;
}else{
dummy.next = head2;
}
}
11.鏈表劃分
ListNode partition(ListNode head,int x){
if(head == null){
return null;
}
ListNode left = new ListNode(0);
ListNode right = new ListNode(0);
ListNode leftDummy = left;
ListNode rightDummy = right;
while(head!=null){
if(head.val<x){
left.next = head;
left = head;
}else{
right.next = head;
right = head;
}
head = head.next;
}
left.next = rightDummy.next;
right.next = null;
return leftDummy.next;
}
12.翻轉鏈表的n到m之間的節點
ListNode reverseN2M(ListNode head,int m,int n){
if(m>=n||head == null){
return head;
}
ListNode dummy = new ListNode(0);
dummy.next = head;
head = dummy;
for(int i = 1;i<m;i++){
if(head == null){
return null;
}
head = head.next;
}
ListNode pmNode = head;
ListNode mNode = head.next;
ListNode nNode = mNode;
ListNode pnNode = mNode.next;
for(int i = m;i<n;i++){
if(pnNode == null){
return null;
}
ListNode tmp = pnNode.next;
pnNode.next = nNode;
nNode = pnNode;
pnNode = tmp;
}
pmNode.next = nNode;
mNode.next = pnNode;
return dummy.next;
}
13.合併K個排序過的鏈表
ListNode mergeKListNode(ArrayList<ListNode> k){
if(k.size()==0){
return null;
}
return mergeHelper(k,0,k.size()-1);
}
ListNode mergeHelper(List<ListNode> lists,int start,int end){
if(start == end){
return lists.get(start);
}
int mid = start + ( end - start )/2;
ListNode left = mergeHelper(lists, start, mid);
ListNode right = mergeHelper(lists, mid+1, end);
return mergeTwoLists(left,right);
}
ListNode mergeTwoLists(ListNode list1,ListNode list2){
ListNode dummy = new ListNode(0);
ListNode tail = dummy;
while(list1!=null&&list2!=null){
if(list1.val<list2.val){
tail.next = list1;
tail = tail.next;
list1 = list1.next;
}else{
tail.next = list2;
tail = list2;
list2 = list2.next;
}
}
if(list1!=null){
tail.next = list1;
}else{
tail.next = list2;
}
return dummy.next;
}
一篇文章搞定面試中的二叉樹題目
最近總結了一些數據結構和算法相關的題目,這是第一篇文章,關於二叉樹的。
先上二叉樹的數據結構:
class TreeNode{
int val;
//左孩子
TreeNode left;
//右孩子
TreeNode right;
}
二叉樹的題目普遍可以用遞歸和迭代的方式來解
1.求二叉樹的最大深度
int maxDeath(TreeNode node){
if(node==null){
return 0;
}
int left = maxDeath(node.left);
int right = maxDeath(node.right);
return Math.max(left,right) + 1;
}
2.求二叉樹的最小深度
int getMinDepth(TreeNode root){
if(root == null){
return 0;
}
return getMin(root);
}
int getMin(TreeNode root){
if(root == null){
return Integer.MAX_VALUE;
}
if(root.left == null&&root.right == null){
return 1;
}
return Math.min(getMin(root.left),getMin(root.right)) + 1;
}
3,求二叉樹中節點的個數
int numOfTreeNode(TreeNode root){
if(root == null){
return 0;
}
int left = numOfTreeNode(root.left);
int right = numOfTreeNode(root.right);
return left + right + 1;
}
4,求二叉樹中葉子節點的個數
int numsOfNoChildNode(TreeNode root){
if(root == null){
return 0;
}
if(root.left==null&&root.right==null){
return 1;
}
return numsOfNodeTreeNode(root.left)+numsOfNodeTreeNode(root.right);
}
5.求二叉樹中第k層節點的個數
int numsOfkLevelTreeNode(TreeNode root,int k){
if(root == null||k<1){
return 0;
}
if(k==1){
return 1;
}
int numsLeft = numsOfkLevelTreeNode(root.left,k-1);
int numsRight = numsOfkLevelTreeNode(root.right,k-1);
return numsLeft + numsRight;
}
6.判斷二叉樹是否是平衡二叉樹
boolean isBalanced(TreeNode node){
return maxDeath2(node)!=-1;
}
int maxDeath2(TreeNode node){
if(node == null){
return 0;
}
int left = maxDeath2(node.left);
int right = maxDeath2(node.right);
if(left==-1||right==-1||Math.abs(left-right)>1){
return -1;
}
return Math.max(left, right) + 1;
}
7.判斷二叉樹是否是完全二叉樹
什麼是完全二叉樹呢?參見
boolean isCompleteTreeNode(TreeNode root){
if(root == null){
return false;
}
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.add(root);
boolean result = true;
boolean hasNoChild = false;
while(!queue.isEmpty()){
TreeNode current = queue.remove();
if(hasNoChild){
if(current.left!=null||current.right!=null){
result = false;
break;
}
}else{
if(current.left!=null&¤t.right!=null){
queue.add(current.left);
queue.add(current.right);
}else if(current.left!=null&¤t.right==null){
queue.add(current.left);
hasNoChild = true;
}else if(current.left==null&¤t.right!=null){
result = false;
break;
}else{
hasNoChild = true;
}
}
}
return result;
}
8.兩個二叉樹是否完全相同
boolean isSameTreeNode(TreeNode t1,TreeNode t2){
if(t1==null&&t2==null){
return true;
}
else if(t1==null||t2==null){
return false;
}
if(t1.val != t2.val){
return false;
}
boolean left = isSameTreeNode(t1.left,t2.left);
boolean right = isSameTreeNode(t1.right,t2.right);
return left&&right;
}
9.兩個二叉樹是否互爲鏡像
boolean isMirror(TreeNode t1,TreeNode t2){
if(t1==null&&t2==null){
return true;
}
if(t1==null||t2==null){
return false;
}
if(t1.val != t2.val){
return false;
}
return isMirror(t1.left,t2.right)&&isMirror(t1.right,t2.left);
}
10.翻轉二叉樹or鏡像二叉樹
TreeNode mirrorTreeNode(TreeNode root){
if(root == null){
return null;
}
TreeNode left = mirrorTreeNode(root.left);
TreeNode right = mirrorTreeNode(root.right);
root.left = right;
root.right = left;
return root;
}
11.求兩個二叉樹的最低公共祖先節點
TreeNode getLastCommonParent(TreeNode root,TreeNode t1,TreeNode t2){
if(findNode(root.left,t1)){
if(findNode(root.right,t2)){
return root;
}else{
return getLastCommonParent(root.left,t1,t2);
}
}else{
if(findNode(root.left,t2)){
return root;
}else{
return getLastCommonParent(root.right,t1,t2)
}
}
}
// 查找節點node是否在當前 二叉樹中
boolean findNode(TreeNode root,TreeNode node){
if(root == null || node == null){
return false;
}
if(root == node){
return true;
}
boolean found = findNode(root.left,node);
if(!found){
found = findNode(root.right,node);
}
return found;
}
12.二叉樹的前序遍歷
迭代解法
ArrayList<Integer> preOrder(TreeNode root){
Stack<TreeNode> stack = new Stack<TreeNode>();
ArrayList<Integer> list = new ArrayList<Integer>();
if(root == null){
return list;
}
stack.push(root);
while(!stack.empty()){
TreeNode node = stack.pop();
list.add(node.val);
if(node.right!=null){
stack.push(node.right);
}
if(node.left != null){
stack.push(node.left);
}
}
return list;
}
遞歸解法
ArrayList<Integer> preOrderReverse(TreeNode root){
ArrayList<Integer> result = new ArrayList<Integer>();
preOrder2(root,result);
return result;
}
void preOrder2(TreeNode root,ArrayList<Integer> result){
if(root == null){
return;
}
result.add(root.val);
preOrder2(root.left,result);
preOrder2(root.right,result);
}
13.二叉樹的中序遍歷
ArrayList<Integer> inOrder(TreeNode root){
ArrayList<Integer> list = new ArrayList<<Integer>();
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode current = root;
while(current != null|| !stack.empty()){
while(current != null){
stack.add(current);
current = current.left;
}
current = stack.peek();
stack.pop();
list.add(current.val);
current = current.right;
}
return list;
}
14.二叉樹的後序遍歷
ArrayList<Integer> postOrder(TreeNode root){
ArrayList<Integer> list = new ArrayList<Integer>();
if(root == null){
return list;
}
list.addAll(postOrder(root.left));
list.addAll(postOrder(root.right));
list.add(root.val);
return list;
}
15.前序遍歷和後序遍歷構造二叉樹
TreeNode buildTreeNode(int[] preorder,int[] inorder){
if(preorder.length!=inorder.length){
return null;
}
return myBuildTree(inorder,0,inorder.length-1,preorder,0,preorder.length-1);
}
TreeNode myBuildTree(int[] inorder,int instart,int inend,int[] preorder,int prestart,int preend){
if(instart>inend){
return null;
}
TreeNode root = new TreeNode(preorder[prestart]);
int position = findPosition(inorder,instart,inend,preorder[start]);
root.left = myBuildTree(inorder,instart,position-1,preorder,prestart+1,prestart+position-instart);
root.right = myBuildTree(inorder,position+1,inend,preorder,position-inend+preend+1,preend);
return root;
}
int findPosition(int[] arr,int start,int end,int key){
int i;
for(i = start;i<=end;i++){
if(arr[i] == key){
return i;
}
}
return -1;
}
16.在二叉樹中插入節點
TreeNode insertNode(TreeNode root,TreeNode node){
if(root == node){
return node;
}
TreeNode tmp = new TreeNode();
tmp = root;
TreeNode last = null;
while(tmp!=null){
last = tmp;
if(tmp.val>node.val){
tmp = tmp.left;
}else{
tmp = tmp.right;
}
}
if(last!=null){
if(last.val>node.val){
last.left = node;
}else{
last.right = node;
}
}
return root;
}
17.輸入一個二叉樹和一個整數,打印出二叉樹中節點值的和等於輸入整數所有的路徑
void findPath(TreeNode r,int i){
if(root == null){
return;
}
Stack<Integer> stack = new Stack<Integer>();
int currentSum = 0;
findPath(r, i, stack, currentSum);
}
void findPath(TreeNode r,int i,Stack<Integer> stack,int currentSum){
currentSum+=r.val;
stack.push(r.val);
if(r.left==null&&r.right==null){
if(currentSum==i){
for(int path:stack){
System.out.println(path);
}
}
}
if(r.left!=null){
findPath(r.left, i, stack, currentSum);
}
if(r.right!=null){
findPath(r.right, i, stack, currentSum);
}
stack.pop();
}
18.二叉樹的搜索區間
給定兩個值 k1 和 k2(k1 < k2)和一個二叉查找樹的根節點。找到樹中所有值在 k1 到 k2 範圍內的節點。即打印所有x (k1 <= x <= k2) 其中 x 是二叉查找樹的中的節點值。返回所有升序的節點值。
ArrayList<Integer> result;
ArrayList<Integer> searchRange(TreeNode root,int k1,int k2){
result = new ArrayList<Integer>();
searchHelper(root,k1,k2);
return result;
}
void searchHelper(TreeNode root,int k1,int k2){
if(root == null){
return;
}
if(root.val>k1){
searchHelper(root.left,k1,k2);
}
if(root.val>=k1&&root.val<=k2){
result.add(root.val);
}
if(root.val<k2){
searchHelper(root.right,k1,k2);
}
}
19.二叉樹的層次遍歷
ArrayList<ArrayList<Integer>> levelOrder(TreeNode root){
ArrayList<ArrayList<Integer>> result = new ArrayList<ArrayList<Integer>>();
if(root == null){
return result;
}
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(root);
while(!queue.isEmpty()){
int size = queue.size();
ArrayList<<Integer> level = new ArrayList<Integer>():
for(int i = 0;i < size ;i++){
TreeNode node = queue.poll();
level.add(node.val);
if(node.left != null){
queue.offer(node.left);
}
if(node.right != null){
queue.offer(node.right);
}
}
result.add(Level);
}
return result;
}
20.二叉樹內兩個節點的最長距離
二叉樹中兩個節點的最長距離可能有三種情況:
1.左子樹的最大深度+右子樹的最大深度爲二叉樹的最長距離
2.左子樹中的最長距離即爲二叉樹的最長距離
3.右子樹種的最長距離即爲二叉樹的最長距離
因此,遞歸求解即可
private static class Result{
int maxDistance;
int maxDepth;
public Result() {
}
public Result(int maxDistance, int maxDepth) {
this.maxDistance = maxDistance;
this.maxDepth = maxDepth;
}
}
int getMaxDistance(TreeNode root){
return getMaxDistanceResult(root).maxDistance;
}
Result getMaxDistanceResult(TreeNode root){
if(root == null){
Result empty = new Result(0,-1);
return empty;
}
Result lmd = getMaxDistanceResult(root.left);
Result rmd = getMaxDistanceResult(root.right);
Result result = new Result();
result.maxDepth = Math.max(lmd.maxDepth,rmd.maxDepth) + 1;
result.maxDistance = Math.max(lmd.maxDepth + rmd.maxDepth,Math.max(lmd.maxDistance,rmd.maxDistance));
return result;
}
21.不同的二叉樹
給出 n,問由 1…n 爲節點組成的不同的二叉查找樹有多少種?
int numTrees(int n ){
int[] counts = new int[n+2];
counts[0] = 1;
counts[1] = 1;
for(int i = 2;i<=n;i++){
for(int j = 0;j<i;j++){
counts[i] += counts[j] * counts[i-j-1];
}
}
return counts[n];
}
22.判斷二叉樹是否是合法的二叉查找樹(BST)
一棵BST定義爲:
節點的左子樹中的值要嚴格小於該節點的值。
節點的右子樹中的值要嚴格大於該節點的值。
左右子樹也必須是二叉查找樹。
一個節點的樹也是二叉查找樹。
public int lastVal = Integer.MAX_VALUE;
public boolean firstNode = true;
public boolean isValidBST(TreeNode root) {
// write your code here
if(root==null){
return true;
}
if(!isValidBST(root.left)){
return false;
}
if(!firstNode&&lastVal >= root.val){
return false;
}
firstNode = false;
lastVal = root.val;
if (!isValidBST(root.right)) {
return false;
}
return true;
}
深刻的理解這些題的解法思路,在面試中的二叉樹題目就應該沒有什麼問題。
作者:IOExceptioner
鏈接:http://www.jianshu.com/p/0190985635eb
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
關於Java面試,你應該準備這些知識
從《關於Java面試,你應該準備這些知識點》 一文的閱讀量和點贊程度可以發現,貌似大家更喜歡這類文章,也許是技術型的文章看着比較的枯燥,這些只是我近段時間求職面試時所遇到的一些問題,整理出來希望對有需要的同學提供幫助,可以更系統的去學習各個知識點。
虛擬機JVM相關
這塊內容並非每個面試官都會問,但是如果是應聘高級職位的話,這一環節是不可缺少的,面試的難易程度也不一樣,有些面試官或許讓你講講虛擬機的內存模型即可,有些也會讓你解釋垃圾回收的實現,當然也會有虛擬機調優的實戰經驗,線上問題排查等等。
場景對話:
面試官:Java虛擬機有了解麼?
我:恩,略有接觸過…(水哥說過,話不能說太滿,容易打臉)
面試官:那你先講講它的內存模型吧
我:Java堆,Java棧,程序計數器,方法區,1.7的永久代,1.8的metaspace….(噼裏啪啦概念講一通,簡短描述下每個內存區的用途,能想到的都講出來,不要保留,不要等面試官問 “還有嗎?”)
面試官:好,一般Java堆是如何實現的?
我:在HotSpot虛擬機實現中,Java堆分成了新生代和老年代,我當時看的是1.7的實現,所有還有永久代,新生代中又分爲了eden區和survivor區,survivor區又分成了S0和S1,或則是from和to,(這個時候,我要求紙和筆,因爲我覺得這個話題可以聊蠻長時間,又是我比較熟悉的…一邊畫圖,一邊描述),其中eden,from和to的內存大小默認是8:1:1(各種細節都要說出來…),此時,我已經在紙上畫出了新生代和老年代代表的區域
面試官:恩,給我講講對象在內存中的初始化過程?
我:(千萬不要只說,新對象在Java堆進行內存分配並初始化,或是在eden區進行內存分配並初始化)要初始化一個對象,首先要加載該對象所對應的class文件,該文件的數據會被加載到永久代,並創建一個底層的instanceKlass對象代表該class,再爲將要初始化的對象分配內存空間,優先在線程私有內存空間中分配大小,如果空間不足,再到eden中進行內存分配…^&&*%
面試官:恩,好,說下YGC的大概過程…
我:先找出根對象,如Java棧中引用的對象、靜態變量引用的對象和系統詞典中引用的對象等待,把這些對象標記成活躍對象,並複製到to區,接着遍歷這些活躍對象中引用的對象並標記,找出老年代對象在eden區有引用關係的對象並標記,最後把這些標記的對象複製到to,在複製過程還要判斷活躍對象的gc年齡是否已經達到閾值,如果已經達到閾值,就直接晉升到老年代,YGC結束之後把from和to的引用互換(能多說點就多說點,省的面試官再提問,我把老年代的cms回收也大致說了一遍,以爲面試官會跳過這個話題了,還是太年輕了)。
面試官:你剛剛說到在YGC的時候,有些對象可能會發生晉升,如果晉升失敗怎麼處理?
我:….(斷片了幾秒鐘,我記得我分析過這段代碼的,但是印象不深刻了)我記得在標記階段時,會把對象和對應的對象頭數據保存在兩個棧中,如果晉升失敗的話,就把該對象的對象頭復原…
面試官:那你在實際項目中有碰到這種情況麼,會導致什麼問題?
我:…(這我真沒有遇到過)對,有遇到過一次,在分析gc日誌的時候,發現YGC發生之後,日誌顯示gc後的內存變大了,後來查出來是因爲對象的晉升失敗造成的。(我隱約記得看過笨神的一篇文章,回答的心裏很虛)
面試官:(沒有反駁,繼續問)有過虛擬機性能調優的經驗麼?
我:(說實話,調優經驗真的不多)恩,有一點吧,不是很足,就是我們XX項目上線的時候,發現YGC特別的頻繁^^&^8&,通過調整新生代的大小(線上環境的虛擬機參數是默認的),同時檢查業務邏輯代碼&*&$$~~!
面試官:恩?還有麼?
我:(面試這麼久,好怕面試官的下一句是 “恩?還有麼?”,顯然面試官還不滿足我的回答,但是我也只能答到這個地步了…)恩,經驗確實有限,目前就根據這個項目做過一些相關的優化。
面試官: 。。。。。。
我:。。。。。。
面試官: 那我們看看別的吧。
關於虛擬機方面的文章,我針對hotSpot的實現寫了一些分析,感興趣的同學可以看看,這些文章看着確實有點枯燥。
相關文章:
JVM源碼分析之JVM啓動流程
JVM源碼分析之堆內存的初始化
JVM源碼分析之Java類的加載過程
JVM源碼分析之Java對象的創建過程
JVM源碼分析之如何觸發並執行GC線程
JVM源碼分析之垃圾收集的執行過程
JVM源碼分析之新生代DefNewGeneration的實現
JVM源碼分析之老年代TenuredGeneration的垃圾回收算法實現
細節問題
細節決定成敗,在面試過程中,雖然也有運氣的成分存在,但是對於細節的掌握程度,可以很好的衡量應試者的技術水平。
volatile
場景對話:
面試官:說說volatile關鍵字的實現原理
我:volatile關鍵字提供了內存可見性和禁止內存重排序
面試官:分別解釋一下
我:因爲在虛擬機內存中有主內存和工作內存的概念,每個cpu都有自己的工作內存,當讀取一個普通變量時,優先讀取工作內存的變量,如果工作內存中沒有對應的變量,則從主內存中加載到工作內存,對工作內存的普通變量進行修改,不會立馬同步到主內存,內存可見性保證了在多線程的場景下,保證了線程A對變量的修改,其它線程可以讀到最新值&&%%……
面試官:如何保證的?
我:當對volatile修飾的變量進行寫操作時,直接把最新值寫到主內存中,並清空其它cpu工作內存中該變量所在的內存行數據,當對volatile修飾的變量進行讀操作時,會讀取主內存的數據&&&%%¥@
面試官:你知道系統級別是如何實現的麼?
我:(what,what are u 說啥呢)我記得操作volatile變量的彙編代碼前面會有lock前綴指令
面試官:你這說的還是代碼層面,我說的是系統級別
我:(懵逼臉…)這個再底層下去我真的沒研究過了…
相關文章:《java volatile關鍵字解惑》
Object.finalize
場景對話:
面試官:和我講講Object類的finalize方法的實現原理
我:(完全沒想到面試官會問這個)新建一個對象時,在JVM中會判斷該對象對應的類是否重寫了finalize方法,且finalize方法體不爲空,則把該對象封裝成Finalizer對象,並添加到Finalizer鏈表。
面試官:恩,然後呢?
我:Finalizer類中會初始化一個FinalizerThread類型的線程,負責從一個引用隊列中獲取Finalizer對象,並執行該Finalizer對象的runFinalizer方法,最終會執行原始對象的finalize方法,&&%%##(這塊邏輯有點繞,當時答的也有點虛)
面試官:Finalizer對象什麼時候會在引用隊列中?
我:(努力回想中)在發生GC的時候,具體在什麼時間點或如何被插入到引用隊列中,這塊實現我已經忘記了…(我真的忘記了,只記得這塊邏輯太複雜了)
面試官:恩,你驗證過finalize方法是否會執行麼?
我:恩,自己寫過例子證明過,也看過源碼的實現。
面試官:怎麼證明的?
我:初始化一個大數組,可以明顯看出gc之後是否被回收,然後執行System.gc(),在finalize方法中輸出信息 &&%%@@,(把之前做過的驗證說一遍)
面試官:恩,可以…
相關文章: 《深入分析Object.finalize方法的實現原理》
大問題
什麼是大問題,就是問題很大,讓你自己去理解,把你的畢生所學都拿出來.
場景對話:
面試官:如果給你一個系統,如何去優化?
我:(優化什麼?性能,穩定性,還是其它方面,只能硬着頭皮上了,結合自己做的一個項目)
1、分析系統,定義指標
2、通過系統埋點,收集指標的度量值,對指標進行迭代優化&&^%&$#
面試官:就這些?沒了麼?
我:(因爲是電話面試,感覺當時腦袋是空白的,估計和面試官的級別也有關係)如果指標是接口性能的話,可以看下系統內存是不是可以使用緩存進行性能上的優化,比如redis,如果是訪問很頻繁又不會經常變動的數據,如熱點數據,可以直接使用本地緩存進行優化,畢竟一次網絡請求也需要1~2毫秒
面試官:沒了麼?
我:(因爲自己系統優化的經驗確實不豐富,讓面試官覺得怎麼就只能想到如此少的優化點呢)數據庫的讀寫分離,數據庫的分庫分表,如果經常條件查詢數據庫的話,可以引入搜索服務es或則lucene進行優化
作者:佔小狼
鏈接:http://www.jianshu.com/p/fa0085a0cdf9
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
mysql問答
1、MySQL的複製原理以及流程
基本原理流程,3個線程以及之間的關聯;
2、MySQL中myisam與innodb的區別,至少5點
(1)、問5點不同;
(2)、innodb引擎的4大特性
(3)、2者selectcount(*)哪個更快,爲什麼
3、MySQL中varchar與char的區別以及varchar(50)中的50代表的涵義
(1)、varchar與char的區別
(2)、varchar(50)中50的涵義
(3)、int(20)中20的涵義
(4)、mysql爲什麼這麼設計
4、問了innodb的事務與日誌的實現方式
(1)、有多少種日誌;
(2)、事物的4種隔離級別
(3)、事務是如何通過日誌來實現的,說得越深入越好。
5、問了MySQL binlog的幾種日誌錄入格式以及區別
(1)、binlog的日誌格式的種類和分別
(2)、適用場景;
(3)、結合第一個問題,每一種日誌格式在複製中的優劣。
6、問了下MySQL數據庫cpu飆升到500%的話他怎麼處理?
(1)、沒有經驗的,可以不問;
(2)、有經驗的,問他們的處理思路。
7、sql優化
(1)、explain出來的各種item的意義;
(2)、profile的意義以及使用場景;
8、備份計劃,mysqldump以及xtranbackup的實現原理
(1)、備份計劃;
(2)、備份恢復時間;
(3)、xtrabackup實現原理
9、mysqldump中備份出來的sql,如果我想sql文件中,一行只有一個insert….value()的話,怎麼辦?如果備份需要帶上master的複製點信息怎麼辦?
10、500臺db,在最快時間之內重啓
.
11、innodb的讀寫參數優化
(1)、讀取參數
(2)、寫入參數;
(3)、與IO相關的參數;
(4)、緩存參數以及緩存的適用場景。
12、你是如何監控你們的數據庫的?你們的慢日誌都是怎麼查詢的?
.
13、你是否做過主從一致性校驗,如果有,怎麼做的,如果沒有,你打算怎麼做?
14、你們數據庫是否支持emoji表情,如果不支持,如何操作?
.
15、你是如何維護數據庫的數據字典的?
16、你們是否有開發規範,如果有,如何執行的
17、表中有大字段X(例如:text類型),且字段X不會經常更新,以讀爲爲主,請問
(1)、您是選擇拆成子表,還是繼續放一起;
(2)、寫出您這樣選擇的理由。
18、MySQL中InnoDB引擎的行鎖是通過加在什麼上完成(或稱實現)的?爲什麼是這樣子的?
.
19、如何從mysqldump產生的全庫備份中只恢復某一個庫、某一張表?
開放性問題:據說是騰訊的
一個6億的表a,一個3億的表b,通過外間tid關聯,你如何最快的查詢出滿足條件的第50000到第50200中的這200條數據記錄。
Part4:答案
1、MySQL的複製原理以及流程
基本原理流程,3個線程以及之間的關聯;
1. 主:binlog線程——記錄下所有改變了數據庫數據的語句,放進master上的binlog中;
2. 從:io線程——在使用start slave 之後,負責從master上拉取 binlog 內容,放進 自己的relay log中;
3. 從:sql執行線程——執行relay log中的語句;
2、MySQL中myisam與innodb的區別,至少5點
(1)、問5點不同;
1>.InnoDB支持事物,而MyISAM不支持事物
2>.InnoDB支持行級鎖,而MyISAM支持表級鎖
3>.InnoDB支持MVCC, 而MyISAM不支持
4>.InnoDB支持外鍵,而MyISAM不支持
5>.InnoDB不支持全文索引,而MyISAM支持。
(2)、innodb引擎的4大特性
插入緩衝(insert buffer),二次寫(double write),自適應哈希索引(ahi),預讀(read ahead)
(3)、2者selectcount(*)哪個更快,爲什麼
myisam更快,因爲myisam內部維護了一個計數器,可以直接調取。
3、MySQL中varchar與char的區別以及varchar(50)中的50代表的涵義
(1)、varchar與char的區別
char是一種固定長度的類型,varchar則是一種可變長度的類型
(2)、varchar(50)中50的涵義
最多存放50個字符,varchar(50)和(200)存儲hello所佔空間一樣,但後者在排序時會消耗更多內存,因爲order by col採用fixed_length計算col長度(memory引擎也一樣)
(3)、int(20)中20的涵義
是指顯示字符的長度
但要加參數的,最大爲255,比如它是記錄行數的id,插入10筆資料,它就顯示00000000001 ~~~00000000010,當字符的位數超過11,它也只顯示11位,如果你沒有加那個讓它未滿11位就前面加0的參數,它不會在前面加0
20表示最大顯示寬度爲20,但仍佔4字節存儲,存儲範圍不變;
(4)、mysql爲什麼這麼設計
對大多數應用沒有意義,只是規定一些工具用來顯示字符的個數;int(1)和int(20)存儲和計算均一樣;
4、問了innodb的事務與日誌的實現方式
(1)、有多少種日誌;
錯誤日誌:記錄出錯信息,也記錄一些警告信息或者正確的信息。
查詢日誌:記錄所有對數據庫請求的信息,不論這些請求是否得到了正確的執行。
慢查詢日誌:設置一個閾值,將運行時間超過該值的所有SQL語句都記錄到慢查詢的日誌文件中。
二進制日誌:記錄對數據庫執行更改的所有操作。
中繼日誌:
事務日誌:
(2)、事物的4種隔離級別
隔離級別
讀未提交(RU)
讀已提交(RC)
可重複讀(RR)
串行
(3)、事務是如何通過日誌來實現的,說得越深入越好。
事務日誌是通過redo和innodb的存儲引擎日誌緩衝(Innodb log buffer)來實現的,當開始一個事務的時候,會記錄該事務的lsn(log sequence number)號; 當事務執行時,會往InnoDB存儲引擎的日誌
的日誌緩存裏面插入事務日誌;當事務提交時,必須將存儲引擎的日誌緩衝寫入磁盤(通過innodb_flush_log_at_trx_commit來控制),也就是寫數據前,需要先寫日誌。這種方式稱爲“預寫日誌方式”
5、問了MySQL binlog的幾種日誌錄入格式以及區別
(1)、binlog的日誌格式的種類和分別
(2)、適用場景;
(3)、結合第一個問題,每一種日誌格式在複製中的優劣。
Statement:每一條會修改數據的sql都會記錄在binlog中。
優點:不需要記錄每一行的變化,減少了binlog日誌量,節約了IO,提高性能。(相比row能節約多少性能 與日誌量,這個取決於應用的SQL情況,正常同一條記錄修改或者插入row格式所產生的日誌量還小於Statement產生的日誌量,但是考慮到如果帶條 件的update操作,以及整表刪除,alter表等操作,ROW格式會產生大量日誌,因此在考慮是否使用ROW格式日誌時應該跟據應用的實際情況,其所 產生的日誌量會增加多少,以及帶來的IO性能問題。)
缺點:由於記錄的只是執行語句,爲了這些語句能在slave上正確運行,因此還必須記錄每條語句在執行的時候的 一些相關信息,以保證所有語句能在slave得到和在master端執行時候相同 的結果。另外mysql 的複製,像一些特定函數功能,slave可與master上要保持一致會有很多相關問題(如sleep()函數, last_insert_id(),以及user-defined functions(udf)會出現問題).
使用以下函數的語句也無法被複制:
* LOAD_FILE()
* UUID()
* USER()
* FOUND_ROWS()
* SYSDATE() (除非啓動時啓用了 –sysdate-is-now 選項)
同時在INSERT …SELECT 會產生比 RBR 更多的行級鎖
2.Row:不記錄sql語句上下文相關信息,僅保存哪條記錄被修改。
優點: binlog中可以不記錄執行的sql語句的上下文相關的信息,僅需要記錄那一條記錄被修改成什麼了。所以rowlevel的日誌內容會非常清楚的記錄下 每一行數據修改的細節。而且不會出現某些特定情況下的存儲過程,或function,以及trigger的調用和觸發無法被正確複製的問題
缺點:所有的執行的語句當記錄到日誌中的時候,都將以每行記錄的修改來記錄,這樣可能會產生大量的日誌內容,比 如一條update語句,修改多條記錄,則binlog中每一條修改都會有記錄,這樣造成binlog日誌量會很大,特別是當執行alter table之類的語句的時候,由於表結構修改,每條記錄都發生改變,那麼該表每一條記錄都會記錄到日誌中。
3.Mixedlevel: 是以上兩種level的混合使用,一般的語句修改使用statment格式保存binlog,如一些函數,statement無法完成主從複製的操作,則 採用row格式保存binlog,MySQL會根據執行的每一條具體的sql語句來區分對待記錄的日誌形式,也就是在Statement和Row之間選擇 一種.新版本的MySQL中隊row level模式也被做了優化,並不是所有的修改都會以row level來記錄,像遇到表結構變更的時候就會以statement模式來記錄。至於update或者delete等修改數據的語句,還是會記錄所有行的 變更。
6、問了下MySQL數據庫cpu飆升到500%的話他怎麼處理?
(1)、沒有經驗的,可以不問;
(2)、有經驗的,問他們的處理思路。
列出所有進程 show processlist 觀察所有進程 多秒沒有狀態變化的(幹掉)
查看超時日誌或者錯誤日誌 (做了幾年開發,一般會是查詢以及大批量的插入會導致cpu與i/o上漲,,,,當然不排除網絡狀態突然斷了,,導致一個請求服務器只接受到一半,比如where子句或分頁子句沒有發送,,當然的一次被坑經歷)
7、sql優化
(1)、explain出來的各種item的意義;
select_type
表示查詢中每個select子句的類型
type
表示MySQL在表中找到所需行的方式,又稱“訪問類型”
possible_keys
指出MySQL能使用哪個索引在表中找到行,查詢涉及到的字段上若存在索引,則該索引將被列出,但不一定被查詢使用
key
顯示MySQL在查詢中實際使用的索引,若沒有使用索引,顯示爲NULL
key_len
表示索引中使用的字節數,可通過該列計算查詢中使用的索引的長度
ref
表示上述表的連接匹配條件,即哪些列或常量被用於查找索引列上的值
Extra
包含不適合在其他列中顯示但十分重要的額外信息
(2)、profile的意義以及使用場景;
查詢到 SQL 會執行多少時間, 並看出 CPU/Memory 使用量, 執行過程中 Systemlock, Table lock 花多少時間等等
8、備份計劃,mysqldump以及xtranbackup的實現原理
(1)、備份計劃;
這裏每個公司都不一樣,您別說那種1小時1全備什麼的就行
(2)、備份恢復時間;
這裏跟機器,尤其是硬盤的速率有關係,以下列舉幾個僅供參考
20G的2分鐘(mysqldump)
80G的30分鐘(mysqldump)
111G的30分鐘(mysqldump)
288G的3小時(xtra)
3T的4小時(xtra)
邏輯導入時間一般是備份時間的5倍以上
(3)、xtrabackup實現原理
在InnoDB內部會維護一個redo日誌文件,我們也可以叫做事務日誌文件。事務日誌會存儲每一個InnoDB表數據的記錄修改。當InnoDB啓動時,InnoDB會檢查數據文件和事務日誌,並執行兩個步驟:它應用(前滾)已經提交的事務日誌到數據文件,並將修改過但沒有提交的數據進行回滾操作。
9、mysqldump中備份出來的sql,如果我想sql文件中,一行只有一個insert….value()的話,怎麼辦?如果備份需要帶上master的複製點信息怎麼辦?
–skip-extended-insert
[root@helei-zhuanshu ~]# mysqldump -uroot -p helei –skip-extended-insert
Enter password:
KEY `idx_c1` (`c1`),
KEY `idx_c2` (`c2`)
) ENGINE=InnoDB AUTO_INCREMENT=51 DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
—
— Dumping data for table `helei`
—
LOCK TABLES `helei` WRITE;
/*!40000 ALTER TABLE `helei` DISABLE KEYS */;
INSERT INTO `helei` VALUES (1,32,37,38,’2016-10-18 06:19:24′,’susususususususususususu’);
INSERT INTO `helei` VALUES (2,37,46,21,’2016-10-18 06:19:24′,’susususususu’);
INSERT INTO `helei` VALUES (3,21,5,14,’2016-10-18 06:19:24′,’susu’);
10、500臺db,在最快時間之內重啓
puppet,dsh
11、innodb的讀寫參數優化
(1)、讀取參數
global buffer pool以及 local buffer;
(2)、寫入參數;
innodb_flush_log_at_trx_commit
innodb_buffer_pool_size
(3)、與IO相關的參數;
innodb_write_io_threads = 8
innodb_read_io_threads = 8
innodb_thread_concurrency = 0
(4)、緩存參數以及緩存的適用場景。
query cache/query_cache_type
並不是所有表都適合使用query cache。造成query cache失效的原因主要是相應的table發生了變更
- 第一個:讀操作多的話看看比例,簡單來說,如果是用戶清單表,或者說是數據比例比較固定,比如說商品列表,是可以打開的,前提是這些庫比較集中,數據庫中的實務比較小。
- 第二個:我們“行騙”的時候,比如說我們競標的時候壓測,把query cache打開,還是能收到qps激增的效果,當然前提示前端的連接池什麼的都配置一樣。大部分情況下如果寫入的居多,訪問量並不多,那麼就不要打開,例如社交網站的,10%的人產生內容,其餘的90%都在消費,打開還是效果很好的,但是你如果是qq消息,或者聊天,那就很要命。
- 第三個:小網站或者沒有高併發的無所謂,高併發下,會看到 很多 qcache 鎖 等待,所以一般高併發下,不建議打開query cache
12、你是如何監控你們的數據庫的?你們的慢日誌都是怎麼查詢的?
監控的工具有很多,例如zabbix,lepus,我這裏用的是lepus
13、你是否做過主從一致性校驗,如果有,怎麼做的,如果沒有,你打算怎麼做?
主從一致性校驗有多種工具 例如checksum、mysqldiff、pt-table-checksum等
14、你們數據庫是否支持emoji表情,如果不支持,如何操作?
如果是utf8字符集的話,需要升級至utf8_mb4方可支持
15、你是如何維護數據庫的數據字典的?
這個大家維護的方法都不同,我一般是直接在生產庫進行註釋,利用工具導出成excel方便流通。
16、你們是否有開發規範,如果有,如何執行的
有,開發規範網上有很多了,可以自己看看總結下
17、表中有大字段X(例如:text類型),且字段X不會經常更新,以讀爲爲主,請問
(1)、您是選擇拆成子表,還是繼續放一起;
(2)、寫出您這樣選擇的理由。
答:拆帶來的問題:連接消耗 + 存儲拆分空間;不拆可能帶來的問題:查詢性能;
如果能容忍拆分帶來的空間問題,拆的話最好和經常要查詢的表的主鍵在物理結構上放置在一起(分區) 順序IO,減少連接消耗,最後這是一個文本列再加上一個全文索引來儘量抵消連接消耗
如果能容忍不拆分帶來的查詢性能損失的話:上面的方案在某個極致條件下肯定會出現問題,那麼不拆就是最好的選擇
18、MySQL中InnoDB引擎的行鎖是通過加在什麼上完成(或稱實現)的?爲什麼是這樣子的?
答:InnoDB是基於索引來完成行鎖
例: select * from tab_with_index where id = 1 for update;
for update 可以根據條件來完成行鎖鎖定,並且 id 是有索引鍵的列,
如果 id 不是索引鍵那麼InnoDB將完成表鎖,,併發將無從談起
.
19、如何從mysqldump產生的全庫備份中只恢復某一個庫、某一張表?
答案見:http://suifu.blog.51cto.com/9167728/1830651
開放性問題:據說是騰訊的
一個6億的表a,一個3億的表b,通過外間tid關聯,你如何最快的查詢出滿足條件的第50000到第50200中的這200條數據記錄。
1、如果A表TID是自增長,並且是連續的,B表的ID爲索引
select * from a,b where a.tid = b.id and a.tid>500000 limit 200;
2、如果A表的TID不是連續的,那麼就需要使用覆蓋索引.TID要麼是主鍵,要麼是輔助索引,B表ID也需要有索引。
select * from b , (select tid from a limit 50000,200) a where b.id = a .tid;
shell處理mysql增、刪、改、查
引言
這幾天做一個任務,比對兩個數據表中的數據,昨天用PHP寫了一個版本,但考慮到有的機器沒有php或者php沒有編譯mysql擴展,就無法使用mysql系列的函數,腳本就無效了,今天寫個shell版本的,這樣,在所有linux系列機器上就都可以運行了。
shell是如何操作mysql的?
shell操作mysql其實就是通過mysql命令通過參數去執行語句,跟其他程序裏面是一樣的,看看下面這個參數:
-e, --execute=name Execute command and quit. (Disables --force and history file.)
因此我們可以通過mysql -e來執行語句,就像下面這樣:
mysql -hlocalhost -P3306 -uroot -p123456 $test --default-character-set=utf8 -e "select * from users"
執行之後返回下面結果:
在shell腳本中操作mysql
導出數據
MYSQL="mysql -h192.168.1.102 -uroot -p123456 --default-character-set=utf8 -A -N"
#這裏面有兩個參數,-A、-N,-A的含義是不去預讀全部數據表信息,這樣可以解決在數據表很多的時候卡死的問題
#-N,很簡單,Don't write column names in results,獲取的數據信息省去列名稱
sql="select * from test.user"
result="$($MYSQL -e "$sql")"
dump_data=./data.user.txt
>$dump_data
echo -e "$result" > $dump_data
#這裏要額外注意,echo -e "$result" > $dump_data的時候一定要加上雙引號,不讓導出的數據會擠在一行
#下面是返回的測試數據
3 吳彥祖 32
5 王力宏 32
6 ab 32
7 黃曉明 33
8 anonymous 32
插入數據
#先看看要導入的數據格式,三列,分別是id,名字,年齡(數據是隨便捏造的),放入data.user.txt
12 tf 23
13 米勒 24
14 西安電子科技大學 90
15 西安交大 90
16 北京大學 90
#OLF_IFS=$IFS
#IFS=","
#臨時設置默認分隔符爲逗號
cat data.user.txt | while read id name age
do
sql="insert into test.user(id, name, age) values(${id}, '${name}', ${age});"
$MYSQL -e "$sql"
done
輸出結果
+----+--------------------------+-----+
| id | name | age |
+----+--------------------------+-----+
| 12 | tf | 23 |
| 13 | 米勒 | 24 |
| 14 | 西安電子科技大學 | 90 |
| 15 | 西安交大 | 90 |
| 16 | 北京大學 | 90 |
+----+--------------------------+-----+
更新數據
#先看看更新數據的格式,將左邊一列替換爲右邊一列,只有左邊一列的刪除,下面數據放入update.user.txt
tf twoFile
西安電子科技大學 西軍電
西安交大 西安交通大學
北京大學
cat update.user.txt | while read src dst
do
if [ ! -z "${src}" -a ! -z "${dst}" ]
then
sql="update test.user set name='${dst}' where name='${src}'"
fi
if [ ! -z "${src}" -a -z "${dst}" ]
then
sql="delete from test.user where name='${src}'"
fi
$MYSQL -e "$sql"
done
輸出結果:
+----+--------------------------+-----+
| id | name | age |
+----+--------------------------+-----+
| 12 | twoFile | 23 |
| 13 | 米勒 | 24 |
| 14 | 西軍電 | 90 |
| 15 | 西安交通大學 | 90 |
+----+--------------------------+-----+
dump數據到sql文件
#利用mysqldump這個命令可以很輕鬆的導出所有數據的sql語句到指定文件
#導出root@localhost下面的exp.Opes中的所有數據到tt.sql
mysqldump -h localhost -u root -p exp Opes > ./tt.sql
#回車之後輸入密碼就可以將所有sql語句輸出到tt.sql
導入數據到mysql數據庫
#設置編碼,不然可能出現亂碼
mysql -hlocalhost -uroot --default-character-set=gbk -p exp< ./tt.sql
#回車之後輸入密碼,導入tt.sql中的所有數據到exp數據庫中