1. 程式人生 > 其它 >面試題系列三:多執行緒(必問)

面試題系列三:多執行緒(必問)

1.建立執行緒有哪些方法(4種)?

  • 繼承Thread類,重寫run方法(其實Thread類本身也實現了Runnable介面)

  • 實現Runnable介面,重寫run方法

  • 實現Callable介面,重寫call方法(有返回值)

  • 使用執行緒池(有返回值)

2.執行緒的生命週期

執行緒的生命週期包含5個階段,包括:新建、就緒、執行、阻塞、銷燬。

  • 新建:就是剛使用new方法,new出來的執行緒;

  • 就緒:就是呼叫的執行緒的start()方法後,這時候執行緒處於等待CPU分配資源階段,誰先搶的CPU資源,誰開始執行;

  • 執行:當就緒的執行緒被排程並獲得CPU資源時,便進入執行狀態,run方法定義了執行緒的操作和功能;

  • 阻塞:在執行狀態的時候,可能因為某些原因導致執行狀態的執行緒變成了阻塞狀態,比如sleep()、wait()之後執行緒就處於了阻塞狀態,這個時候需要其他機制將處於阻塞狀態的執行緒喚醒,比如呼叫notify或者notifyAll()方法。喚醒的執行緒不會立刻執行run方法,它們要再次等待CPU分配資源進入執行狀態;

  • 銷燬:如果執行緒正常執行完畢後或執行緒被提前強制性的終止或出現異常導致結束,那麼執行緒就要被銷燬,釋放資源;

完整的生命週期圖如下:

3.Runnable和Callable的區別?

  • 相同點:兩者都需要呼叫Thread.start啟動執行緒;

  • 不同點:

    Callable介面的call()方法有返回值;而實現Runnable介面的run()方法沒有返回值;

    Callable介面的call()方法允許丟擲異常;而Runnable介面的run()方法的異常只能在內部消化,不能繼續上拋;

4.start()和run()方法有什麼區別?

start()方法被用來啟動新建立的執行緒,而且start()內部呼叫了run()方法;

run( )方法是一個普通方法,當你呼叫run()方法的時候,只會是在原來的執行緒中呼叫,沒有新的執行緒啟動。

  1. start()方法功能介紹

    start()方法來啟動執行緒,真正實現了多執行緒執行。

    start方法的作用就是將執行緒由建立狀態,變為就緒狀態。當執行緒建立成功時,執行緒處於建立狀態,如果你不呼叫start( )方法,那麼執行緒永遠處於建立狀態。呼叫start( )後,才會變為就緒狀態,執行緒才可以被CPU執行。

  2. start()執行時間

    呼叫start( )方法後,執行緒的狀態是就緒狀態,而不是執行狀態(關於執行緒的狀態詳細。執行緒要等待CPU排程,不同的

    JVM有不同的排程演算法,執行緒何時被排程是未知的。因此,start()方法的被呼叫順序不能決定執行緒的執行順序。

注意:

由於線上程的生命週期中,執行緒的狀態由建立到就緒只會發生一次,因此,一個執行緒只能呼叫start()方法一次,多次

啟動一個執行緒是非法的。特別是當執行緒已經結束執行後,不能再重新啟動。

5.執行緒池有哪些引數

  • 最常用的三個引數:

    corePoolSize:核心執行緒數

    queueCapacity:任務佇列容量(阻塞佇列)

    maxPoolSize:最大執行緒數

  • 三個引數的作用:

    當執行緒數小於核心執行緒數時,建立執行緒;

    當執行緒數大於等於核心執行緒數,且任務佇列未滿時,將任務放入任務佇列;

    當執行緒數大於等於核心執行緒數,且任務佇列已滿

    1. 若執行緒數小於最大執行緒數,建立執行緒

    2. 若執行緒數等於最大執行緒數,丟擲異常,拒絕任務

6.執行緒池有幾種(5種)?拒絕策略有幾種(4種)?阻塞佇列有幾種(3種)?

  • 五種執行緒池:

  1. 1. ExecutorService threadPool = null;
    2. threadPool = Executors.newCachedThreadPool();//有緩衝的執行緒池,執行緒數 JVM 控制
    3. threadPool = Executors.newFixedThreadPool(3);//固定大小的執行緒池
    4. threadPool = Executors.newScheduledThreadPool(2);//一個能實現定時、週期性任務的執行緒池
    5. threadPool = Executors.newSingleThreadExecutor();//單執行緒的執行緒池,只有一個執行緒在工作
    6. threadPool = new ThreadPoolExecutor();//預設執行緒池,可控制引數比較多   
  • 四種拒絕策略:

  1. 1. RejectedExecutionHandler rejected = null;
    2. rejected = new ThreadPoolExecutor.AbortPolicy();//預設,佇列滿了丟任務丟擲異常
    3. rejected = new ThreadPoolExecutor.DiscardPolicy();//佇列滿了丟任務不異常
    4. rejected = new ThreadPoolExecutor.DiscardOldestPolicy();//將最早進入佇列的任務刪,之後再嘗試加入佇列
    5. rejected = new ThreadPoolExecutor.CallerRunsPolicy();//如果新增到執行緒池失敗,那麼主執行緒會自己去執行該任務
  • 三種阻塞佇列:

  1. 1. BlockingQueue<Runnable> workQueue = null;
    2. workQueue = new ArrayBlockingQueue<>(5);//基於陣列的先進先出佇列,有界
    3. workQueue = new LinkedBlockingQueue<>();//基於連結串列的先進先出佇列,無界
    4. workQueue = new SynchronousQueue<>();//無緩衝的等待佇列,無界

7.死鎖

7.1 什麼叫死鎖?

所謂死鎖,是指多個程序在執行過程中因爭奪資源而造成的一種僵局,當程序處於這種僵持狀態時,若無外力作用,它們

都將無法再向前推進。舉個例子來描述,如果此時有一個執行緒A,按照先鎖a再獲得鎖b的的順序獲得鎖,而在此同時又有

另外一個執行緒B,按照先鎖b再鎖a的順序獲得鎖。

7.2 產生死鎖的原因?

  1. 因為系統資源不足。

  2. 程序執行推進的順序不合適。

  3. 資源分配不當等。

7.3 死鎖產生的4個必要條件?

  1. 互斥條件:程序要求對所分配的資源進行排它性控制,即在一段時間內某資源僅為一程序所佔用。

  2. 請求和保持條件:當程序因請求資源而阻塞時,對已獲得的資源保持不放。

  3. 不剝奪條件:程序已獲得的資源在未使用完之前,不能剝奪,只能在使用完時由自己釋放。

  4. 環路等待條件:在發生死鎖時,必然存在一個程序--資源的環形鏈。

7.4 預防和處理死鎖的方法?

(1)儘量不要在釋放鎖之前競爭其他鎖 一般可以通過細化同步方法來實現,只在真正需要保護共享資源的地方去拿鎖,並儘快釋放鎖,這樣可以有效降低 在同步方法裡呼叫其他同步方法的情況

(2)順序索取鎖資源 如果實在無法避免巢狀索取鎖資源,則需要制定一個索取鎖資源的策略,先規劃好有哪些鎖,然後各個執行緒按照一個順序去索取,不要出現上面那個例子中不同順序,這樣就會有潛在的死鎖問題

(3)嘗試定時鎖 在索取鎖的時候可以設定一個超時時間,如果超過這個時間還沒索取到鎖,則不會繼續堵塞而是放棄此次任務

解除死鎖的方法?
(1)剝奪資源:從其它程序剝奪足夠數量的資源給死鎖程序,以解除死鎖狀態;
(2)撤消程序:可以直接撤消死鎖程序或撤消代價最小的程序,直至有足夠的資源可用,死鎖狀態.消除為止;所謂代價是指優先順序、執行代價、程序的重要性和價值等。
如何檢測死鎖?
(1)利用Java自帶工具JConsole
(2)Java執行緒死鎖檢視分析方法

8.volatile底層是怎麼實現的?

當一個變數定義為volatile後,它將具備兩種特性:1. 可見性,2. 禁止指令重排序。

可見性:編譯器為了加快程式執行速度,對一些變數的寫操作會現在暫存器或CPU快取上進行,最後寫入記憶體。而在這個過程中,變數的新值對其它執行緒是不可見的。當對volatile標記的變數進行修改時,先當前處理器快取行的資料寫回到系統記憶體,然後這個寫回記憶體的操作會使其他CPU裡快取了該記憶體地址的資料無效。
處理器使用嗅探技術保證它的內部快取、系統記憶體和其他處理器的快取的資料在總線上保持一致。如果一個正在共享的狀態的地址被嗅探到其他處理器打算寫記憶體地址,那麼正在嗅探的處理器將使它的快取行無效,在下次訪問相同記憶體地址時,強制執行快取行填充。

9.volatile與synchronized有什麼區別?

  • volatile僅能使用在變數上,synchronized則可以使用在方法、類、同步程式碼塊等等。

  • volatile只能保證可見性和有序性,不能保證原子性。而synchronized都可以保證。

volatile不會造成執行緒的阻塞,而synchronized可能會造成執行緒的阻塞.

10.wait()和sleep()的區別?

  1. sleep()不釋放鎖,wait()釋放鎖
  2. sleep()在Thread類中宣告的,wait()在Object類中宣告
  3. sleep()是靜態方法,是Thread.sleep(); wait()是非靜態方法,必須由“同步鎖”物件呼叫
  4. sleep()方法導致當前執行緒進入阻塞狀態後,當時間到或interrupt()醒來;wait()方法導致當前執行緒進入阻塞狀態後,由notify或notifyAll()

11.樂觀鎖和悲觀鎖的理解及如何實現,有哪些實現方式?

悲觀鎖:總是假設最壞的情況,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會阻塞直到它拿到鎖。傳統的關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。再比如 Java 裡面的同步原語 synchronized 關鍵字的實現也是悲觀鎖。

樂觀鎖:顧名思義,就是很樂觀,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,可以使用版本號等機制。樂觀鎖適用於多讀的應用型別,這樣可以提高吞吐量,像資料庫提供的類似於 write_condition 機制,其實都是提供的樂觀鎖。在 Java中 java.util.concurrent.atomic 包下面的原子變數類就是使用了樂觀鎖的一種實現方式 CAS 實現的。

樂觀鎖的實現方式:

1、使用版本標識來確定讀到的資料與提交時的資料是否一致。提交後修改版本標識,不一致時可以採取丟棄和再次嘗試的策略。

2、java 中的 Compare and Swap 即 CAS ,當多個執行緒嘗試使用 CAS 同時更新同一個變數時,只有其中一個執行緒能更新變數的值,而其它執行緒都失敗,失敗的執行緒並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。 CAS 操作中包含三個運算元 —— 需要讀寫的記憶體位置(V)、進行比較的預期原值(A)和擬寫入的新值(B)。如果記憶體位置 V 的值與預期原值 A 相匹配,那麼處理器會自動將該位置值更新為新值 B。否則處理器不做任何操作。

12.什麼是可重入鎖(ReentrantLock)?

ReentrantLock重入鎖,是實現Lock介面的一個類,也是在實際程式設計中使用頻率很高的一個鎖,支援重入性,表示能夠對共享資源能夠重複加鎖,即當前執行緒獲取該鎖再次獲取不會被阻塞。

在java關鍵字synchronized隱式支援重入性,synchronized通過獲取自增,釋放自減的方式實現重入。與此同時,ReentrantLock還支援公平鎖和非公平鎖兩種方式。那麼,要想完完全全的弄懂ReentrantLock的話,主要也就是ReentrantLock同步語義的學習:1. 重入性的實現原理;2. 公平鎖和非公平鎖。

重入性的實現原理

要想支援重入性,就要解決兩個問題:

  1. 線上程獲取鎖的時候,如果已經獲取鎖的執行緒是當前執行緒的話則直接再次獲取成功;

  2. 由於鎖會被獲取n次,那麼只有鎖在被釋放同樣的n次之後,該鎖才算是完全釋放成功。

ReentrantLock支援兩種鎖:公平鎖和非公平鎖

何謂公平性,是針對獲取鎖而言的,如果一個鎖是公平的,那麼鎖的獲取順序就應該符合請求上的絕對時間順序,滿足FIFO。