從 JDK 源碼角度看 java 併發的公平性

Java爲簡化開發者開發提供了不少併發的工具,包括各類同步器,有了JDK咱們只要學會簡單使用類API便可。但這並不意味着不須要探索其具體的實現機制,本文從JDK源碼角度簡單講講併發時線程競爭的公平性。javascript

所謂公平性指全部線程對臨界資源申請訪問權限的成功率都同樣,不會讓某些線程擁有優先權。咱們知道CLH Node FIFO等待隊列是一個先進先出的隊列,那麼是否就能夠說每條線程獲取鎖時就是公平的呢?關於公平性這裏分拆成三個點分別闡述:java

  1. 準備入隊列的節點,此狀況討論的是線程加入等待隊列時產生的競爭是否公平,線程在嘗試獲取鎖失敗後將被加入等待隊列,這時多個線程經過自旋將節點加入隊列,全部線程在自旋過程當中是沒法保證其公平性的,可能後來的線程比早到的先進入隊列,因此節點入隊列不具公平性。
  2. 等待隊列中的節點,狀況①中成功加入隊列後即成爲等待隊列中的節點,咱們知道此隊列是一個先入先出隊列,那麼很簡單能獲得,隊列中的全部節點是公平的,他們都按照順序等待本身被前驅節點喚醒並獲取鎖,因此等待隊列中的節點具備公平性。
  3. 闖入的節點,這種狀況是指一個新線程到達共享資源邊界時無論等待隊列中是否存在其餘等待節點它都將優先嚐試去獲取鎖,這種稱爲可闖入策略。可闖入特性破壞了公平性,JDK的AQS對外體現的公平性主要由此體現,下面將對闖入特性展開分析。

AQS提供的基礎獲取鎖算法是一種可闖入的算法,即若是有新線程到來先進行一次獲取嘗試,不成功的狀況下才將當前線程加入等待隊列。如圖2-5-9-6所示,等待隊列中節點線程按照順序一個接一個嘗試去獲取共享資源的使用權,某時刻頭結點線程準備嘗試獲取的同時另一條線程闖入,此線程並不是直接加入等待隊列的尾部,而是先跟頭結點線程競爭獲取資源,闖入線程若是成功獲取共享資源則直接執行,頭結點線程則繼續等待下一次嘗試,如此一來闖入線程成功插隊,後來的線程比早到的線程先執行,說明AQS基礎獲取算法是不嚴格公平的。node

基礎獲取算法邏輯簡化以下:首先嚐試獲取鎖,假如獲取失敗才建立節點並加入到等待隊列的尾部,接着經過不斷循環檢查是否輪到本身執行,固然此過程爲了提升性能可能將線程先掛起,最終由前驅節點喚醒。算法

if(嘗試獲取鎖失敗) {
    建立node
    使用CAS方式把node插入到隊列尾部
    while(true){
    if(嘗試獲取鎖成功 而且 node的前驅節點爲頭節點){
把當前節點設置爲頭節點
    跳出循環
}else{
    使用CAS方式修改node前驅節點的waitStatus標識爲signal
    if(修改爲功)
        掛起當前線程 
}
}複製代碼

爲何要使用闖入策略?可闖入的策略一般能夠提供更高的總吞吐量。因爲通常同步器顆粒度比較小,也能夠說共享資源的範圍較小,而線程從阻塞狀態到被喚醒所消耗的時間週期多是經過共享資源時間週期的幾倍甚至幾十倍,如此一來線程喚醒過程當中將存在一個很大的時間週期空窗期,致使資源沒有獲得充分利用,爲了提升吞吐量,引入這種闖入策略,它可使在等待隊列頭結點從阻塞到被喚醒的時間段內闖入的線程直接獲取鎖並經過同步器,以便充分利用喚醒過程這一空窗期,大大增長了吞吐率。另外,闖入機制的實現對外提供一種競爭調節機制,即開發者能夠在自定義同步器中定義闖入嘗試獲取的次數,假設次數爲n則不斷重複獲取直到n次都獲取不成功才把線程加入等待隊列中,隨着次數n的增長能夠增大成功闖入的概率。同時,這種闖入策略可能致使等待隊列中的線程飢餓,由於鎖可能一直被闖入的線程獲取,但因爲通常持有同步器的時間很短暫而避免飢餓的發生,反之若是保護的代碼體很長而且持有同步器的時間較長,這將大大增長等待隊列無限等待的風險。併發

在實際狀況中仍是要根據用戶需求制定策略,在一個公平性要求很高的場景,則能夠把闖入策略去除掉以達到公平。在自定義同步器中能夠經過AQS預留方法tryAcquire方法實現,只需判斷當前線程是否爲等待隊列中頭結點對應的線程,若不是則直接返回false,嘗試獲取失敗。但前面這種公平性是相對Java語法語義層面上的公平性,在現實中JDK的實現會直接影響線程執行的順序。工具

歡迎關注:性能

相關文章
相關標籤/搜索