1. 程式人生 > >想進大廠?50個多執行緒面試題,你會多少?(一)

想進大廠?50個多執行緒面試題,你會多少?(一)

最近看到網上流傳著,各種面試經驗及面試題,往往都是一大堆技術題目貼上去,而沒有答案。

不管你是新程式設計師還是老手,你一定在面試中遇到過有關執行緒的問題。Java語言一個重要的特點就是內建了對併發的支援,讓Java大受企業和程式設計師的歡迎。大多數待遇豐厚的Java開發職位都要求開發者精通多執行緒技術並且有豐富的Java程式開發、除錯、優化經驗,所以執行緒相關的問題在面試中經常會被提到。
在典型的Java面試中, 面試官會從執行緒的基本概念問起

如:為什麼你需要使用執行緒, 如何建立執行緒,用什麼方式建立執行緒比較好(比如:繼承thread類還是呼叫Runnable介面),然後逐漸問到併發問題像在Java併發程式設計的過程中遇到了什麼挑戰,Java記憶體模型,JDK1.5引入了哪些更高階的併發工具,併發程式設計常用的設計模式,經典多執行緒問題如生產者消費者,哲學家就餐,讀寫器或者簡單的有界緩衝區問題。僅僅知道執行緒的基本概念是遠遠不夠的, 你必須知道如何處理死鎖,競態條件,記憶體衝突和執行緒安全等併發問題。掌握了這些技巧,你就可以輕鬆應對多執行緒和併發面試了。
許多Java程式設計師在面試前才會去看面試題,這很正常。

因為收集面試題和練習很花時間,所以我從許多面試者那裡收集了Java多執行緒和併發相關的50個熱門問題。

  • 我陸續會更新更多的面試知識點
  • 請關注公眾號 “搜雲庫” 獲取最新文章

關注公眾號-搜雲庫

下面是Java執行緒相關的熱門面試題,你可以用它來好好準備面試。

  1. 什麼是執行緒?
  2. 什麼是執行緒安全和執行緒不安全?
  3. 什麼是自旋鎖?
  4. 什麼是Java記憶體模型?
  5. 什麼是CAS?
  6. 什麼是樂觀鎖和悲觀鎖?
  7. 什麼是AQS?
  8. 什麼是原子操作?在Java Concurrency API中有哪些原子類(atomic classes)?
  9. 什麼是Executors框架?
  10. 什麼是阻塞佇列?如何使用阻塞佇列來實現生產者-消費者模型?
  11. 什麼是Callable和Future?
  12. 什麼是FutureTask?
  13. 什麼是同步容器和併發容器的實現?
  14. 什麼是多執行緒?優缺點?
  15. 什麼是多執行緒的上下文切換?
  16. ThreadLocal的設計理念與作用?
  17. ThreadPool(執行緒池)用法與優勢?
  18. Concurrent包裡的其他東西:ArrayBlockingQueue、CountDownLatch等等。
  19. synchronized和ReentrantLock的區別?
  20. Semaphore有什麼作用?
  21. Java Concurrency API中的Lock介面(Lock interface)是什麼?對比同步它有什麼優勢?
  22. Hashtable的size()方法中明明只有一條語句”return count”,為什麼還要做同步?
  23. ConcurrentHashMap的併發度是什麼?
  24. ReentrantReadWriteLock讀寫鎖的使用?
  25. CyclicBarrier和CountDownLatch的用法及區別?
  26. LockSupport工具?
  27. Condition介面及其實現原理?
  28. Fork/Join框架的理解?
  29. wait()和sleep()的區別?
  30. 執行緒的五個狀態(五種狀態,建立、就緒、執行、阻塞和死亡)?
  31. start()方法和run()方法的區別?
  32. Runnable介面和Callable介面的區別?
  33. volatile關鍵字的作用?
  34. Java中如何獲取到執行緒dump檔案?
  35. 執行緒和程序有什麼區別?
  36. 執行緒實現的方式有幾種(四種)?
  37. 高併發、任務執行時間短的業務怎樣使用執行緒池?併發不高、任務執行時間長的業務怎樣使用執行緒池?併發高、業務執行時間長的業務怎樣使用執行緒池?
  38. 如果你提交任務時,執行緒池佇列已滿,這時會發生什麼?
  39. 鎖的等級:方法鎖、物件鎖、類鎖?
  40. 如果同步塊內的執行緒丟擲異常會發生什麼?
  41. 併發程式設計(concurrency)並行程式設計(parallellism)有什麼區別?
  42. 如何保證多執行緒下 i++ 結果正確?
  43. 一個執行緒如果出現了執行時異常會怎麼樣?
  44. 如何在兩個執行緒之間共享資料?
  45. 生產者消費者模型的作用是什麼?
  46. 怎麼喚醒一個阻塞的執行緒?
  47. Java中用到的執行緒排程演算法是什麼
  48. 單例模式的執行緒安全性?
  49. 執行緒類的構造方法、靜態塊是被哪個執行緒呼叫的?
  50. 同步方法和同步塊,哪個是更好的選擇?
  51. 如何檢測死鎖?怎麼預防死鎖?

什麼是執行緒?

執行緒是作業系統能夠進行運算排程的最小單位,它被包含在程序之中,是程序中的實際運作單位,可以使用多執行緒對進行運算提速。

比如,如果一個執行緒完成一個任務要100毫秒,那麼用十個執行緒完成改任務只需10毫秒

什麼是執行緒安全和執行緒不安全?

通俗的說:加鎖的就是是執行緒安全的,不加鎖的就是是執行緒不安全的

執行緒安全

執行緒安全: 就是多執行緒訪問時,採用了加鎖機制,當一個執行緒訪問該類的某個資料時,進行保護,其他執行緒不能進行訪問,直到該執行緒讀取完,其他執行緒才可使用。不會出現資料不一致或者資料汙染

一個執行緒安全的計數器類的同一個例項物件在被多個執行緒使用的情況下也不會出現計算失誤。很顯然你可以將集合類分成兩組,執行緒安全和非執行緒安全的
Vector 是用同步方法來實現執行緒安全的, 而和它相似的ArrayList不是執行緒安全的。

執行緒不安全

執行緒不安全:就是不提供資料訪問保護,有可能出現多個執行緒先後更改資料造成所得到的資料是髒資料

如果你的程式碼所在的程序中有多個執行緒在同時執行,而這些執行緒可能會同時執行這段程式碼。如果每次執行結果和單執行緒執行的結果是一樣的,而且其他的變數的值也和預期的是一樣的,就是執行緒安全的。

執行緒安全問題都是由全域性變數及靜態變數引起的。
若每個執行緒中對全域性變數、靜態變數只有讀操作,而無寫操作,一般來說,這個全域性變數是執行緒安全的;若有多個執行緒同時執行寫操作,一般都需要考慮執行緒同步,否則的話就可能影響執行緒安全。

什麼是自旋鎖?

基本概念

自旋鎖是SMP架構中的一種low-level的同步機制

當執行緒A想要獲取一把自選鎖而該鎖又被其它執行緒鎖持有時,執行緒A會在一個迴圈中自選以檢測鎖是不是已經可用了。

自選鎖需要注意

  • 由於自旋時不釋放CPU,因而持有自旋鎖的執行緒應該儘快釋放自旋鎖,否則等待該自旋鎖的執行緒會一直在那裡自旋,這就會浪費CPU時間。
  • 持有自旋鎖的執行緒在sleep之前應該釋放自旋鎖以便其它執行緒可以獲得自旋鎖

實現自旋鎖

參考

一個簡單的while就可以滿足你的要求。

目前的JVM實現自旋會消耗CPU,如果長時間不呼叫doNotify方法,doWait方法會一直自旋,CPU會消耗太大。

public class MyWaitNotify3{

  MonitorObject myMonitorObject = new MonitorObject();
  boolean wasSignalled = false;

  public void doWait(){
    synchronized(myMonitorObject){
      while(!wasSignalled){
        try{
          myMonitorObject.wait();
         } catch(InterruptedException e){...}
      }
      //clear signal and continue running.
      wasSignalled = false;
    }
  }

  public void doNotify(){
    synchronized(myMonitorObject){
      wasSignalled = true;
      myMonitorObject.notify();
    }
  }
}

什麼是Java記憶體模型?

Java記憶體模型描述了在多執行緒程式碼中哪些行為是合法的,以及執行緒如何通過記憶體進行互動。它描述了“程式中的變數“ 和 ”從記憶體或者暫存器獲取或儲存它們的底層細節”之間的關係。Java記憶體模型通過使用各種各樣的硬體和編譯器的優化來正確實現以上事情

Java包含了幾個語言級別的關鍵字,包括:volatile, final以及synchronized,目的是為了幫助程式設計師向編譯器描述一個程式的併發需求。Java記憶體模型定義了volatile和synchronized的行為,更重要的是保證了同步的java程式在所有的處理器架構下面都能正確的執行。

“一個執行緒的寫操作對其他執行緒可見”這個問題是因為編譯器對程式碼進行重排序導致的。例如,只要程式碼移動不會改變程式的語義,當編譯器認為程式中移動一個寫操作到後面會更有效的時候,編譯器就會對程式碼進行移動。如果編譯器推遲執行一個操作,其他執行緒可能在這個操作執行完之前都不會看到該操作的結果,這反映了快取的影響。

此外,寫入記憶體的操作能夠被移動到程式裡更前的時候。在這種情況下,其他的執行緒在程式中可能看到一個比它實際發生更早的寫操作。所有的這些靈活性的設計是為了通過給編譯器,執行時或硬體靈活性使其能在最佳順序的情況下來執行操作。在記憶體模型的限定之內,我們能夠獲取到更高的效能。

看下面程式碼展示的一個簡單例子:

ClassReordering {

    int x = 0, y = 0;

    public void writer() {
        x = 1;
        y = 2;
    }

    public void reader() {
        int r1 = y;
        int r2 = x;
    }
}

讓我們看在兩個併發執行緒中執行這段程式碼,讀取Y變數將會得到2這個值。因為這個寫入比寫到X變數更晚一些,程式設計師可能認為讀取X變數將肯定會得到1。但是,寫入操作可能被重排序過。如果重排序發生了,那麼,就能發生對Y變數的寫入操作,讀取兩個變數的操作緊隨其後,而且寫入到X這個操作能發生。程式的結果可能是r1變數的值是2,但是r2變數的值為0。

但是面試官,有時候不這麼認為,認為就是JVM記憶體結構

JVM記憶體結構主要有三大塊:堆記憶體、方法區和棧

堆記憶體是JVM中最大的一塊由年輕代和老年代組成,而年輕代記憶體又被分成三部分,Eden空間、From Survivor空間、To Survivor空間,預設情況下年輕代按照8:1:1的比例來分配;方法區儲存類資訊、常量、靜態變數等資料,是執行緒共享的區域,為與Java堆區分,方法區還有一個別名Non-Heap(非堆);棧又分為java虛擬機器棧和本地方法棧主要用於方法的執行。

JAVA的JVM的記憶體可分為3個區:堆(heap)、棧(stack)和方法區(method)

java堆(Java Heap)

  • 可通過引數 -Xms 和-Xmx設定

    1. Java堆是被所有執行緒共享,是Java虛擬機器所管理的記憶體中最大的一塊 Java堆在虛擬機器啟動時建立
    2. Java堆唯一的目的是存放物件例項,幾乎所有的物件例項和陣列都在這裡
    3. Java堆為了便於更好的回收和分配記憶體,可以細分為:新生代和老年代;再細緻一點的有Eden空間、From Survivor空間、To Survivor區
  • 新生代:包括Eden區、From Survivor區、To Survivor區,系統預設大小Eden:Survivor=8:1。

  • 老年代:在年輕代中經歷了N次垃圾回收後仍然存活的物件,就會被放到年老代中。因此,可以認為年老代中存放的都是一些生命週期較長的物件。

    1. Survivor空間等Java堆可以處在物理上不連續的記憶體空間中,只要邏輯上是連續的即可(就像我們的磁碟空間一樣。在實現時,既可以實現成固定大小的,也可以是可擴充套件的)。

據Java虛擬機器規範的規定,當方法區無法滿足記憶體分配需求時,將丟擲OutOfMemoryError異常

java虛擬機器棧(stack)

可通過引數 棧幀是方法執行期的基礎資料結構棧容量可由-Xss設定

1.Java虛擬機器棧是執行緒私有的,它的生命週期與執行緒相同
1. 每一個方法被呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程。
1. 虛擬機器棧是執行Java方法的記憶體模型(也就是位元組碼)服務:每個方法在執行的同時都會建立一個棧幀用於儲存 區域性變量表運算元棧動態連結方法出口等資訊。

  • 區域性變量表:32位變數槽,存放了編譯期可知的各種基本資料型別、物件引用、returnAddress型別
  • 運算元棧:基於棧的執行引擎,虛擬機器把運算元棧作為它的工作區,大多數指令都要從這裡彈出資料、執行運算,然後把結果壓回運算元棧。
  • 動態連線每個棧幀都包含一個指向執行時常量池(方法區的一部分)中該棧幀所屬方法的引用。持有這個引用是為了支援方法呼叫過程中的動態連線。Class檔案的常量池中有大量的符號引用,位元組碼中的方法呼叫指令就以常量池中指向方法的符號引用為引數。這些符號引用一部分會在類載入階段或第一次使用的時候轉化為直接引用,這種轉化稱為靜態解析。另一部分將在每一次的執行期間轉化為直接應用,這部分稱為動態連線
  • 方法出口:返回方法被呼叫的位置,恢復上層方法的區域性變數和運算元棧,如果無返回值,則把它壓入呼叫者的運算元棧。

    1. 區域性變量表所需的記憶體空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的區域性變數空間是完全確定的。

    2. 在方法執行期間不會改變區域性變量表的大小。主要存放了編譯期可知的各種基本資料型別、物件引用 (reference型別)、returnAddress型別)

java虛擬機器棧,規定了兩種異常狀況:

  1. 如果執行緒請求的深度大於虛擬機器所允許的深度,將丟擲StackOverflowError異常
  2. 如果虛擬機器棧動態擴充套件,而擴充套件時無法申請到足夠的記憶體,就會丟擲OutOfMemoryError異常

本地方法棧

可通過引數 棧容量可由-Xss設定

  1. 虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務。
  2. 本地方法棧則是為虛擬機器使用到的Native方法服務。有的虛擬機器(譬如Sun HotSpot虛擬機器)直接就把本地方法棧和虛擬機器棧合二為一

方法區(Method Area)

可通過引數-XX:MaxPermSize設定

  1. 執行緒共享記憶體區域,用於儲存已被虛擬機器載入的類資訊、常量、靜態變數,即編譯器編譯後的程式碼,方法區也稱持久代(Permanent Generation)

  2. 雖然Java虛擬機器規範把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應該是與Java堆區分開來。

  3. 如何實現方法區,屬於虛擬機器的實現細節,不受虛擬機器規範約束。

  4. 方法區主要存放java類定義資訊,與垃圾回收關係不大,方法區可以選擇不實現垃圾回收,但不是沒有垃圾回收。

  5. 方法區域的記憶體回收目標主要是針對常量池的回收和對型別的解除安裝

  6. 執行時常量池,也是方法區的一部分,虛擬機器載入Class後把常量池中的資料放入執行時常量池

執行時常量池

JDK1.6之前字串常量池位於方法區之中
JDK1.7字串常量池已經被挪到堆之中

可通過引數-XX:PermSize和-XX:MaxPermSize設定

  • 常量池(Constant Pool):常量池資料編譯期被確定,是Class檔案中的一部分。儲存了類、方法、介面等中的常量,當然也包括字串常量
  • 字串池/字串常量池(String Pool/String Constant Pool):是常量池中的一部分,儲存編譯期類中產生的字串型別資料。
  • 執行時常量池(Runtime Constant Pool):方法區的一部分,所有執行緒共享。虛擬機器載入Class後把常量池中的資料放入到執行時常量池。常量池:可以理解為Class檔案之中的資源倉庫,它是Class檔案結構中與其他專案資源關聯最多的資料型別。

    1. 常量池中主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic Reference)。
    2. 字面量:文字字串、宣告為final的常量值等。

    3. 符號引用:類和介面的完全限定名(Fully Qualified Name)、欄位的名稱和描述符(Descriptor)、方法的名稱和描述符。

直接記憶體

可通過-XX:MaxDirectMemorySize指定,如果不指定,則預設與Java堆的最大值(-Xmx指定)一樣

  • 直接記憶體(Direct Memory)並不是虛擬機器執行時資料區的一部分,也不是Java虛擬機器規範中定義的記憶體區域,但是這部分記憶體也被頻繁地使用,而且也可能導致OutOfMemoryError異常出現

總結的簡單一點

java堆(Java Heap)

可通過引數 -Xms 和-Xmx設定

  1. Java堆是被所有執行緒共享,是Java虛擬機器所管理的記憶體中最大的一塊 Java堆在虛擬機器啟動時建立
  2. Java堆唯一的目的是存放物件例項,幾乎所有的物件例項和陣列都在這裡
  3. Java堆為了便於更好的回收和分配記憶體,可以細分為:新生代和老年代;再細緻一點的有Eden空間、From Survivor空間、To Survivor區

    • 新生代:包括Eden區、From Survivor區、To Survivor區,系統預設大小Eden:Survivor=8:1。
    • 老年代:在年輕代中經歷了N次垃圾回收後仍然存活的物件,就會被放到年老代中。因此,可以認為年老代中存放的都是一些生命週期較長的物件。

java虛擬機器棧(stack)

可通過引數 棧幀是方法執行期的基礎資料結構棧容量可由-Xss設定

  1. Java虛擬機器棧是執行緒私有的,它的生命週期與執行緒相同
  2. 每一個方法被呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程。
  3. 虛擬機器棧是執行Java方法的記憶體模型(也就是位元組碼)服務:每個方法在執行的同時都會建立一個棧幀,用於儲存 區域性變量表運算元棧動態連結方法出口等資訊

方法區(Method Area)

可通過引數-XX:MaxPermSize設定

  1. 執行緒共享記憶體區域),用於儲存已被虛擬機器載入的類資訊、常量、靜態變數,即編譯器編譯後的程式碼方法區也稱持久代(Permanent Generation)

  2. 方法區主要存放java類定義資訊,與垃圾回收關係不大,方法區可以選擇不實現垃圾回收,但不是沒有垃圾回收。

  3. 方法區域的記憶體回收目標主要是針對常量池的回收和對型別的解除安裝。

  4. 執行時常量池,也是方法區的一部分,虛擬機器載入Class後把常量池中的資料放入執行時常量池

什麼是CAS?

CAS(compare and swap)的縮寫,中文翻譯成比較並交換

CAS 不通過JVM,直接利用java本地方 JNI(Java Native Interface為JAVA本地呼叫),直接呼叫CPU 的cmpxchg(是彙編指令)指令。

利用CPU的CAS指令,同時藉助JNI來完成Java的非阻塞演算法,實現原子操作。其它原子操作都是利用類似的特性完成的

整個java.util.concurrent都是建立在CAS之上的,因此對於synchronized阻塞演算法,J.U.C在效能上有了很大的提升。

CAS是項樂觀鎖技術,當多個執行緒嘗試使用CAS同時更新同一個變數時,只有其中一個執行緒能更新變數的值,而其它執行緒都失敗,失敗的執行緒並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。

CAS應用

CAS有3個運算元,記憶體值V,舊的預期值A,要修改的新值B。當且僅當預期值A和記憶體值V相同時,將記憶體值V修改為B,否則什麼都不做。

CAS優點

確保對記憶體的讀-改-寫操作都是原子操作執行

CAS缺點

CAS雖然很高效的解決原子操作,但是CAS仍然存在三大問題。ABA問題,迴圈時間長開銷大和只能保證一個共享變數的原子操作

總結

  1. 使用CAS線上程衝突嚴重時,會大幅降低程式效能;CAS只適合於執行緒衝突較少的情況使用
  2. synchronized在jdk1.6之後,已經改進優化。synchronized的底層實現主要依靠Lock-Free的佇列,基本思路是自旋後阻塞,競爭切換後繼續競爭鎖,稍微犧牲了公平性,但獲得了高吞吐量。線上程衝突較少的情況下,可以獲得和CAS類似的效能;而執行緒衝突嚴重的情況下,效能遠高於CAS

什麼是樂觀鎖和悲觀鎖?

悲觀鎖

Java在JDK1.5之前都是靠synchronized關鍵字保證同步的,這種通過使用一致的鎖定協議來協調對共享狀態的訪問,可以確保無論哪個執行緒持有共享變數的鎖,都採用獨佔的方式來訪問這些變數。獨佔鎖其實就是一種悲觀鎖,所以可以說synchronized是悲觀鎖。

樂觀鎖

樂觀鎖( Optimistic Locking)其實是一種思想。相對悲觀鎖而言,樂觀鎖假設認為資料一般情況下不會造成衝突,所以在資料進行提交更新的時候,才會正式對資料的衝突與否進行檢測,如果發現衝突了,則讓返回使用者錯誤的資訊,讓使用者決定如何去做。

什麼是AQS?

AbstractQueuedSynchronizer簡稱AQS,是一個用於構建鎖和同步容器的框架。事實上concurrent包內許多類都是基於AQS構建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。AQS解決了在實現同步容器時設計的大量細節問題。

AQS使用一個FIFO的隊列表示排隊等待鎖的執行緒,佇列頭節點稱作“哨兵節點”或者“啞節點”,它不與任何執行緒關聯。其他的節點與等待執行緒關聯,每個節點維護一個等待狀態waitStatus。

CAS 原子操作在concurrent包的實現

由於java的CAS同時具有 volatile 讀和volatile寫的記憶體語義,因此Java執行緒之間的通訊現在有了下面四種方式:

  • A執行緒寫volatile變數,隨後B執行緒讀這個volatile變數。
  • A執行緒寫volatile變數,隨後B執行緒用CAS更新這個volatile變數。
  • A執行緒用CAS更新一個volatile變數,隨後B執行緒用CAS更新這個volatile變數。
  • A執行緒用CAS更新一個volatile變數,隨後B執行緒讀這個volatile變數。

Java的CAS會使用現代處理器上提供的高效機器級別原子指令,這些原子指令以原子方式對記憶體執行讀-改-寫操作,這是在多處理器中實現同步的關鍵(從本質上來說,能夠支援原子性讀-改-寫指令的計算機器,是順序計算圖靈機的非同步等價機器,因此任何現代的多處理器都會去支援某種能對記憶體執行原子性讀-改-寫操作的原子指令)。同時,volatile變數的讀/寫和CAS可以實現執行緒之間的通訊。把這些特性整合在一起,就形成了整個concurrent包得以實現的基石。如果我們仔細分析concurrent包的原始碼實現,會發現一個通用化的實現模式:

首先,宣告共享變數為volatile;
然後,使用CAS的原子條件更新來實現執行緒之間的同步;

同時,配合以volatile的讀/寫和CAS所具有的volatile讀和寫的記憶體語義來實現執行緒之間的通訊。

AQS,非阻塞資料結構和原子變數類(Java.util.concurrent.atomic包中的類),這些concurrent包中的基礎類都是使用這種模式來實現的,而concurrent包中的高層類又是依賴於這些基礎類來實現的。從整體來看,concurrent包的實現示意圖如下:

image

AQS沒有鎖之類的概念,它有個state變數,是個int型別,在不同場合有著不同含義。

AQS圍繞state提供兩種基本操作“獲取”和“釋放”,有條雙向佇列存放阻塞的等待執行緒,並提供一系列判斷和處理方法,簡單說幾點:

  • state是獨佔的,還是共享的;
  • state被獲取後,其他執行緒需要等待;
  • state被釋放後,喚醒等待執行緒;
  • 執行緒等不及時,如何退出等待。

至於執行緒是否可以獲得state,如何釋放state,就不是AQS關心的了,要由子類具體實現。

AQS中還有一個表示狀態的欄位state,例如ReentrantLocky用它表示執行緒重入鎖的次數,Semaphore用它表示剩餘的許可數量,FutureTask用它表示任務的狀態。對state變數值的更新都採用CAS操作保證更新操作的原子性

AbstractQueuedSynchronizer繼承了AbstractOwnableSynchronizer,這個類只有一個變數:exclusiveOwnerThread,表示當前佔用該鎖的執行緒,並且提供了相應的get,set方法。

ReentrantLock實現原理

什麼是原子操作?在Java Concurrency API中有哪些原子類(atomic classes)?

原子操作是指一個不受其他操作影響的操作任務單元。原子操作是在多執行緒環境下避免資料不一致必須的手段。

int++並不是一個原子操作,所以當一個執行緒讀取它的值並加1時,另外一個執行緒有可能會讀到之前的值,這就會引發錯誤。

為了解決這個問題,必須保證增加操作是原子的,在JDK1.5之前我們可以使用同步技術來做到這一點。

到JDK1.5,java.util.concurrent.atomic包提供了int和long型別的裝類,它們可以自動的保證對於他們的操作是原子的並且不需要使用同步。
  

什麼是Executors框架?

Executor框架同java.util.concurrent.Executor 介面在Java 5中被引入。

Executor框架是一個根據一組執行策略呼叫,排程,執行和控制的非同步任務的框架。

無限制的建立執行緒會引起應用程式記憶體溢位。所以建立一個執行緒池是個更好的的解決方案,因為可以限制執行緒的數量並且可以回收再利用這些執行緒。

利用Executors框架可以非常方便的建立一個執行緒池,

Java通過Executors提供四種執行緒池,分別為:

newCachedThreadPool建立一個可快取執行緒池,如果執行緒池長度超過處理需要,可靈活回收空閒執行緒,若無可回收,則新建執行緒。

newFixedThreadPool 建立一個定長執行緒池,可控制執行緒最大併發數,超出的執行緒會在佇列中等待。

newScheduledThreadPool 建立一個定長執行緒池,支援定時及週期性任務執行。

newSingleThreadExecutor 建立一個單執行緒化的執行緒池,它只會用唯一的工作執行緒來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先順序)執行。
  

什麼是阻塞佇列?如何使用阻塞佇列來實現生產者-消費者模型?

JDK7提供了7個阻塞佇列。(也屬於併發容器)

  1. ArrayBlockingQueue :一個由陣列結構組成的有界阻塞佇列。
  2. LinkedBlockingQueue :一個由連結串列結構組成的有界阻塞佇列。
  3. PriorityBlockingQueue :一個支援優先順序排序的無界阻塞佇列。
  4. DelayQueue:一個使用優先順序佇列實現的無界阻塞佇列。
  5. SynchronousQueue:一個不儲存元素的阻塞佇列。
  6. LinkedTransferQueue:一個由連結串列結構組成的無界阻塞佇列。
  7. LinkedBlockingDeque:一個由連結串列結構組成的雙向阻塞佇列。

什麼是阻塞佇列?

阻塞佇列是一個在佇列基礎上又支援了兩個附加操作的佇列。

2個附加操作:

支援阻塞的插入方法:佇列滿時,佇列會阻塞插入元素的執行緒,直到佇列不滿。
支援阻塞的移除方法:佇列空時,獲取元素的執行緒會等待佇列變為非空。

阻塞佇列的應用場景

阻塞佇列常用於生產者和消費者的場景,生產者是向佇列裡新增元素的執行緒,消費者是從佇列裡取元素的執行緒。簡而言之,阻塞佇列是生產者用來存放元素、消費者獲取元素的容器。

幾個方法

在阻塞佇列不可用的時候,上述2個附加操作提供了四種處理方法

方法\處理方式 丟擲異常 返回特殊值 一直阻塞 超時退出
插入方法 add(e) offer(e) put(e) offer(e,time,unit)
移除方法 remove() poll() take() poll(time,unit)
檢查方法 element() peek() 不可用 不可用

JAVA裡的阻塞佇列

JDK 7 提供了7個阻塞佇列,如下

1、ArrayBlockingQueue 陣列結構組成的有界阻塞佇列。

此佇列按照先進先出(FIFO)的原則對元素進行排序,但是預設情況下不保證執行緒公平的訪問佇列,即如果佇列滿了,那麼被阻塞在外面的執行緒對佇列訪問的順序是不能保證執行緒公平(即先阻塞,先插入)的。

2、LinkedBlockingQueue一個由連結串列結構組成的有界阻塞佇列

此佇列按照先出先進的原則對元素進行排序

3、PriorityBlockingQueue支援優先順序的無界阻塞佇列

4、DelayQueue支援延時獲取元素的無界阻塞佇列,即可以指定多久才能從佇列中獲取當前元素

5、SynchronousQueue不儲存元素的阻塞佇列,每一個put必須等待一個take操作,否則不能繼續新增元素。並且他支援公平訪問佇列。

6、LinkedTransferQueue由連結串列結構組成的無界阻塞TransferQueue佇列。相對於其他阻塞佇列,多了tryTransfer和transfer方法

transfer方法

如果當前有消費者正在等待接收元素(take或者待時間限制的poll方法),transfer可以把生產者傳入的元素立刻傳給消費者。如果沒有消費者等待接收元素,則將元素放在佇列的tail節點,並等到該元素被消費者消費了才返回。

tryTransfer方法

用來試探生產者傳入的元素能否直接傳給消費者。,如果沒有消費者在等待,則返回false。和上述方法的區別是該方法無論消費者是否接收,方法立即返回。而transfer方法是必須等到消費者消費了才返回。

7、LinkedBlockingDeque連結串列結構的雙向阻塞佇列,優勢在於多執行緒入隊時,減少一半的競爭。

如何使用阻塞佇列來實現生產者-消費者模型?

通知模式實現:所謂通知模式,就是當生產者往滿的佇列裡新增元素時會阻塞住生產者,當消費者消費了一個佇列中的元素後,會通知生產者當前佇列可用。

使用BlockingQueue解決生產者消費者問題

為什麼BlockingQueue適合解決生產者消費者問題

任何有效的生產者-消費者問題解決方案都是通過控制生產者put()方法(生產資源)和消費者take()方法(消費資源)的呼叫來實現的,一旦你實現了對方法的阻塞控制,那麼你將解決該問題。

Java通過BlockingQueue提供了開箱即用的支援來控制這些方法的呼叫(一個執行緒建立資源,另一個消費資源)。java.util.concurrent包下的BlockingQueue介面是一個執行緒安全的可用於存取物件的佇列。

BlockingQueue是一種資料結構,支援一個執行緒往裡存資源,另一個執行緒從裡取資源。這正是解決生產者消費者問題所需要的,那麼讓我們開始解決該問題吧。

生產者

以下程式碼用於生產者執行緒

package io.ymq.example.thread;

import java.util.concurrent.BlockingQueue;

/**
 * 描述:生產者
 *
 * @author yanpenglei
 * @create 2018-03-14 15:52
 **/
class Producer implements Runnable {

    protected BlockingQueue<Object> queue;

    Producer(BlockingQueue<Object> theQueue) {
        this.queue = theQueue;
    }

    public void run() {
        try {
            while (true) {
                Object justProduced = getResource();
                queue.put(justProduced);
                System.out.println("生產者資源佇列大小= " + queue.size());
            }
        } catch (InterruptedException ex) {
            System.out.println("生產者 中斷");
        }
    }

    Object getResource() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException ex) {
            System.out.println("生產者 讀 中斷");
        }
        return new Object();
    }
}

消費者

以下程式碼用於消費者執行緒

package io.ymq.example.thread;

import java.util.concurrent.BlockingQueue;

/**
 * 描述: 消費者
 *
 * @author yanpenglei
 * @create 2018-03-14 15:54
 **/
class Consumer implements Runnable {
    protected BlockingQueue<Object> queue;

    Consumer(BlockingQueue<Object> theQueue) {
        this.queue = theQueue;
    }

    public void run() {
        try {
            while (true) {
                Object obj = queue.take();
                System.out.println("消費者 資源 佇列大小 " + queue.size());
                take(obj);
            }
        } catch (InterruptedException ex) {
            System.out.println("消費者 中斷");
        }
    }

    void take(Object obj) {
        try {
            Thread.sleep(100); // simulate time passing
        } catch (InterruptedException ex) {
            System.out.println("消費者 讀 中斷");
        }
        System.out.println("消費物件 " + obj);
    }
}

測試該解決方案是否執行正常

package io.ymq.example.thread;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * 描述: 測試
 *
 * @author yanpenglei
 * @create 2018-03-14 15:58
 **/
public class ProducerConsumerExample {

    public static void main(String[] args) throws InterruptedException {

        int numProducers = 4;
        int numConsumers = 3;

        BlockingQueue<Object> myQueue = new LinkedBlockingQueue<Object>(5);

        for (int i = 0; i < numProducers; i++) {
            new Thread(new Producer(myQueue)).start();
        }

        for (int i = 0; i < numConsumers; i++) {
            new Thread(new Consumer(myQueue)).start();
        }

        Thread.sleep(1000);

        System.exit(0);
    }
}

執行結果

生產者資源佇列大小= 1
生產者資源佇列大小= 1
消費者 資源 佇列大小 1
生產者資源佇列大小= 1
消費者 資源 佇列大小 1
消費者 資源 佇列大小 1
生產者資源佇列大小= 1
生產者資源佇列大小= 3
消費物件 java.lang.Object@1e1aa52b
生產者資源佇列大小= 2
生產者資源佇列大小= 5
消費物件 java.lang.Object@6e740a76
消費物件 java.lang.Object@697853f6

......

消費物件 java.lang.Object@41a10cbc
消費物件 java.lang.Object@4963c8d1
消費者 資源 佇列大小 5
生產者資源佇列大小= 5
生產者資源佇列大小= 5
消費者 資源 佇列大小 4
消費物件 java.lang.Object@3e49c35d
消費者 資源 佇列大小 4
生產者資源佇列大小= 5

從輸出結果中,我們可以發現佇列大小永遠不會超過5,消費者執行緒消費了生產者生產的資源

什麼是Callable和Future?

Callable 和 Future 是比較有趣的一對組合。當我們需要獲取執行緒的執行結果時,就需要用到它們。Callable用於產生結果,Future用於獲取結果

Callable介面使用泛型去定義它的返回型別。Executors類提供了一些有用的方法去線上程池中執行Callable內的任務。由於Callable任務是並行的,必須等待它返回的結果。java.util.concurrent.Future物件解決了這個問題。

線上程池提交Callable任務後返回了一個Future物件,使用它可以知道Callable任務的狀態和得到Callable返回的執行結果。Future提供了get()方法,等待Callable結束並獲取它的執行結果。

程式碼示例

Callable 是一個介面,它只包含一個call()方法。Callable是一個返回結果並且可能丟擲異常的任務

為了便於理解,我們可以將Callable比作一個Runnable介面,而Callable的call()方法則類似於Runnable的run()方法

public class CallableFutureTest {

    public static void main(String[] args) throws InterruptedException, ExecutionException {

        System.out.println("start main thread ");

        ExecutorService exec = Executors.newFixedThreadPool(2);

        //新建一個Callable 任務,並將其提交到一個ExecutorService. 將返回一個描述任務情況的Future.
        Callable<String> call = new Callable<String>() {

            @Override
            public String call() throws Exception {
                System.out.println("start new thread ");
                Thread.sleep(5000);
                System.out.println("end new thread ");
                return "我是返回的內容";
            }
        };

        Future<String> task = exec.submit(call);
        Thread.sleep(1000);
        String retn = task.get();
        //關閉執行緒池
        exec.shutdown();
        System.out.println(retn + "--end main thread");
    }
}

控制檯列印

start main thread 
start new thread 
end new thread 
我是返回的內容--end main thread

什麼是FutureTask?

FutureTask可用於非同步獲取執行結果或取消執行任務的場景。通過傳入Runnable或者Callable的任務給FutureTask,直接呼叫其run方法或者放入執行緒池執行,之後可以在外部通過FutureTask的get方法非同步獲取執行結果,因此,FutureTask非常適合用於耗時的計算,主執行緒可以在完成自己的任務後,再去獲取結果。另外,FutureTask還可以確保即使呼叫了多次run方法,它都只會執行一次Runnable或者Callable任務,或者通過cancel取消FutureTask的執行等。

1.執行多工計算

FutureTask執行多工計算的使用場景

利用FutureTask和ExecutorService,可以用多執行緒的方式提交計算任務,主執行緒繼續執行其他任務,當主執行緒需要子執行緒的計算結果時,在非同步獲取子執行緒的執行結果。

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class FutureTaskForMultiCompute {

    public static void main(String[] args) {

        FutureTaskForMultiCompute inst = new FutureTaskForMultiCompute();
        // 建立任務集合
        List<FutureTask<Integer>> taskList = new ArrayList<FutureTask<Integer>>();
        // 建立執行緒池
        ExecutorService exec = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            // 傳入Callable物件建立FutureTask物件
            FutureTask<Integer> ft = new FutureTask<Integer>(inst.new ComputeTask(i, "" + i));
            taskList.add(ft);
            // 提交給執行緒池執行任務,也可以通過exec.invokeAll(taskList)一次性提交所有任務;
            exec.submit(ft);
        }

        System.out.println("所有計算任務提交完畢, 主執行緒接著幹其他事情!");

        // 開始統計各計算執行緒計算結果
        Integer totalResult = 0;
        for (FutureTask<Integer> ft : taskList) {
            try {
                //FutureTask的get方法會自動阻塞,直到獲取計算結果為止
                totalResult = totalResult + ft.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }

        // 關閉執行緒池
        exec.shutdown();
        System.out.println("多工計算後的總結果是:" + totalResult);

    }

    private class ComputeTask implements Callable<Integer> {

        private Integer result = 0;
        private String taskName = "";

        public ComputeTask(Integer iniResult, String taskName) {
            result = iniResult;
            this.taskName = taskName;
            System.out.println("生成子執行緒計算任務: " + taskName);
        }

        public String getTaskName() {
            return this.taskName;
        }

        @Override
        public Integer call() throws Exception {
            // TODO Auto-generated method stub

            for (int i = 0; i < 100; i++) {
                result = +i;
            }
            // 休眠5秒鐘,觀察主執行緒行為,預期的結果是主執行緒會繼續執行,到要取得FutureTask的結果是等待直至完成。
            Thread.sleep(5000);
            System.out.println("子執行緒計算任務: " + taskName + " 執行完成!");
            return result;
        }
    }
}
生成子執行緒計算任務: 0
生成子執行緒計算任務: 1
生成子執行緒計算任務: 2
生成子執行緒計算任務: 3
生成子執行緒計算任務: 4
生成子執行緒計算任務: 5
生成子執行緒計算任務: 6
生成子執行緒計算任務: 7
生成子執行緒計算任務: 8
生成子執行緒計算任務: 9
所有計算任務提交完畢, 主執行緒接著幹其他事情!
子執行緒計算任務: 0 執行完成!
子執行緒計算任務: 2 執行完成!
子執行緒計算任務: 3 執行完成!
子執行緒計算任務: 4 執行完成!
子執行緒計算任務: 1 執行完成!
子執行緒計算任務: 8 執行完成!
子執行緒計算任務: 7 執行完成!
子執行緒計算任務: 6 執行完成!
子執行緒計算任務: 9 執行完成!
子執行緒計算任務: 5 執行完成!
多工計算後的總結果是:990

2.高併發環境下

FutureTask在高併發環境下確保任務只執行一次

在很多高併發的環境下,往往我們只需要某些任務只執行一次。這種使用情景FutureTask的特性恰能勝任。舉一個例子,假設有一個帶key的連線池,當key存在時,即直接返回key對應的物件;當key不存在時,則建立連線。對於這樣的應用場景,通常採用的方法為使用一個Map物件來儲存key和連線池對應的對應關係,典型的程式碼如下面所示:

  private Map<String, Connection> connectionPool = new HashMap<String, Connection>();
    private ReentrantLock lock = new ReentrantLock();

    public Connection getConnection(String key) {
        try {
            lock.lock();
            if (connectionPool.containsKey(key)) {
                return connectionPool.get(key);
            } else {
                //建立 Connection  
                Connection conn = createConnection();
                connectionPool.put(key, conn);
                return conn;
            }
        } finally {
            lock.unlock();
        }
    }

    //建立Connection  
    private Connection createConnection() {
        return null;
    }

在上面的例子中,我們通過加鎖確保高併發環境下的執行緒安全,也確保了connection只建立一次,然而確犧牲了效能。改用ConcurrentHash的情況下,幾乎可以避免加鎖的操作,效能大大提高,但是在高併發的情況下有可能出現Connection被建立多次的現象。這時最需要解決的問題就是當key不存在時,建立Connection的動作能放在connectionPool之後執行,這正是FutureTask發揮作用的時機,基於ConcurrentHashMap和FutureTask的改造程式碼如下:

  private ConcurrentHashMap<String, FutureTask<Connection>> connectionPool = new ConcurrentHashMap<String, FutureTask<Connection>>();

    public Connection getConnection(String key) throws Exception {
        FutureTask<Connection> connectionTask = connectionPool.get(key);
        if (connectionTask != null) {
            return connectionTask.get();
        } else {
            Callable<Connection> callable = new Callable<Connection>() {
                @Override
                public Connection call() throws Exception {
                    // TODO Auto-generated method stub  
                    return createConnection();
                }
            };
            FutureTask<Connection> newTask = new FutureTask<Connection>(callable);
            connectionTask = connectionPool.putIfAbsent(key, newTask);
            if (connectionTask == null) {
                connectionTask = newTask;
                connectionTask.run();
            }
            return connectionTask.get();
        }
    }

    //建立Connection  
    private Connection createConnection() {
        return null;
    }

經過這樣的改造,可以避免由於併發帶來的多次建立連線及鎖的出現。

什麼是同步容器和併發容器的實現?

一、同步容器

主要代表有Vector和Hashtable,以及Collections.synchronizedXxx等。
鎖的粒度為當前物件整體。
迭代器是及時失敗的,即在迭代的過程中發現被修改,就會丟擲ConcurrentModificationException。

二、併發容器

主要代表有ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentSkipListMap、ConcurrentSkipListSet。
鎖的粒度是分散的、細粒度的,即讀和寫是使用不同的鎖。
迭代器具有弱一致性,即可以容忍併發修改,不會丟擲ConcurrentModificationException。

JDK 7 ConcurrentHashMap

採用分離鎖技術,同步容器中,是一個容器一個鎖,但在ConcurrentHashMap中,會將hash表的陣列部分分成若干段,每段維護一個鎖,以達到高效的併發訪問;

JDK 8 ConcurrentHashMap

採用分離鎖技術,同步容器中,是一個容器一個鎖,但在ConcurrentHashMap中,會將hash表的陣列部分分成若干段,每段維護一個鎖,以達到高效的併發訪問;

三、阻塞佇列

主要代表有LinkedBlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue(Comparable,Comparator)、SynchronousQueue。
提供了可阻塞的put和take方法,以及支援定時的offer和poll方法。
適用於生產者、消費者模式(執行緒池和工作佇列-Executor),同時也是同步容器

四、雙端佇列

主要代表有ArrayDeque和LinkedBlockingDeque。
意義:正如阻塞佇列適用於生產者消費者模式,雙端佇列同樣適用與另一種模式,即工作密取。在生產者-消費者設計中,所有消費者共享一個工作佇列,而在工作密取中,每個消費者都有各自的雙端佇列。
如果一個消費者完成了自己雙端佇列中的全部工作,那麼他就可以從其他消費者的雙端佇列末尾祕密的獲取工作。具有更好的可伸縮性,這是因為工作者執行緒不會在單個共享的任務佇列上發生競爭。
在大多數時候,他們都只是訪問自己的雙端佇列,從而極大的減少了競爭。當工作者執行緒需要訪問另一個佇列時,它會從佇列的尾部而不是頭部獲取工作,因此進一步降低了佇列上的競爭。
適用於:網頁爬蟲等任務中

五、比較及適用場景

如果不需要阻塞佇列,優先選擇ConcurrentLinkedQueue;
如果需要阻塞佇列,佇列大小固定優先選擇ArrayBlockingQu