源碼閱讀(40):Java中線程安全的Queue、Deque結構——LinkedTransferQueue(3)

(接上文《源碼閱讀(39):Java中線程安全的Queue、Deque結構——LinkedTransferQueue(2)》)

2.4.1、生產者端對xfer方法的調用

請注意我們討論的情況是同時有多個生產者線程,在進行LinkedTransferQueue隊列的數據添加操作。最初,單向鏈表中只有一個虛擬節點,LinkedTransferQueue隊列的head屬性、tail屬性都引用它,如下圖所示
在這裏插入圖片描述
接着,由於是生產者線程調用xfer方法,所以xfer方法的4個方法的特性是:e是該生產者線程添加的數據對象(不爲null);haveData參數的屬性爲true;至於how參數和nanos參數,會有多種值的情況,但是並不影響我們進行討論。接着運行內層for循環時,由於判定條件:

(t != (t = tail) && t.isData == haveData)

的判定結果爲true,所以p變量的引用將以當前tail屬性的引用值爲準。如下圖所示:
在這裏插入圖片描述
按照上文所述,只有當前處理節點p的isData標識和入參的haveData標識一致,且當前處理節點p真實的數據對象存在情況和入參的haveData標識一致,既如下判定式的結果爲true,纔是出隊操作:

p.isData != haveData && haveData == ((item = p.item) == null) {
  // ......
}

很明顯,當多個生產者線程在進行xfer操作時,無論單向鏈表已形成了多少Node節點(Node節點都是存儲任務),在當前操作的入參haveData值爲true時,以上判定式的結果都爲false,所以當前xfer操作不會進入出隊處理邏輯

由於p變量(代表當前正在處理的,處於單向鏈表中的node節點)從tail屬性引用的位置開始,所以在經過以下語句邏輯後,p節點就會指向當前單向鏈表中的最後一個Node節點位置(注意這個“最後一個Node節點”的位置可能並不是tail屬性指向的位置,且這個“最後一個Node節點”的位置可能在本線程操作過程中發生變化——因爲還有其它生產者線程在同時操作)

// ......
// 通過以下的語句模式,p變量所代表的節點終會是某一個瞬時下
// 當前單向鏈表的最後一個Node節點
restart: for (Node s = null, t = null, h = null;;) {
  for (Node p = (t != (t = tail) && t.isData == haveData) ? t : (h = head);; ) {
    // ......
    if ((q = p.next) == null) {
      // ......
    }
    if (p == (p = q)) {
      continue restart;
    }
    // ......
  }
}
// ......

一旦“(q = p.next) == null”的判定是成立,本次xfer操作就開始進行入隊處理邏輯。通過“s = new Node(e);”創建新的Node節點;通過“p.casNext(null, s)”原子操作,試圖將創建的新節點s,成功引用到鏈單向鏈表的末尾;通過“casTail(t, s)”試圖重新爲tail屬性指定新的引用位置——是否成功都無所謂。如下圖是一種可能成功的操作狀態:
在這裏插入圖片描述

2.4.2、消費者端對xfer方法的調用

我們設在多個消費者線程操作前,LinkedTransferQueue中的單向鏈表呈現如下的狀態:
在這裏插入圖片描述
如上圖所示,head引用指向的Node節點是一個“虛”節點,該節點是在LinkedTransferQueue初始化時創建的,其isData屬性的值和item屬性擁有值的真實情況是相悖的——這種特點的節點,將在skipDeadNodesNearHead方法中被清理掉。

那麼當(多個)消費者線程調用xfer方法時,入參e爲null,haveData爲false。起初進入xfer方法時,通過後者外層for循環的初始表達時判定後,局部變量p將被賦值爲head的引用位置,代碼段如下所示:

// 由於tail引用的對象,其isData屬性值與入參haveData不一致
// 所以p變量將被賦值爲head的對象引用
for (Node p = (t != (t = tail) && t.isData == haveData) ? t : (h = head);; ) {
  // ......
}

用圖形化的表達方式,展示如下:
在這裏插入圖片描述
接着,由於head引用的節點是一個“虛”節點,所以p變量的引用位置將基於“q = p.next” 和 “p = q”語句的配合,向鏈表的後續結“移動”,隨後p變量的引用將指向對象編號爲642的Node對象。由於這個Node符合出隊操作的判定式,所以開始執行出隊邏輯:

  // ......
  // 對象id編號爲642的Node結點,其isData屬性值和入參haveData的值相悖
  // 並且其item屬性真實存在數據的情況也和入參haveData的值相悖(注意,不要看到“==”就認爲是相同,請仔細分析判定場景)
  if (p.isData != haveData && haveData == ((item = p.item) == null)) {
    // 出隊邏輯在這裏
    // ......
  }
  // ......

這裏需要注意,由於是多個出隊操作同時進行,所以當前p變量所引用的對象節點的數據可能已經被某個操作線程取出(甚至該節點已經被skipDeadNodesNearHead方法以無效節點的身份清理,變成了自引用狀態),那麼以上表達式可能不成立,需要按照cas的思路重新確認p變量的引用位置,然後再重新開始處理邏輯。請注意出隊邏輯中的如下語句:

// ......
// 由於我們示例的操作場景,單向鏈表由生產者模式下的Node節點構成
// 所以消費者任務進行出隊操作時,以下方法調用成功,將使得p節點item屬性值變爲null。
if (p.tryMatch(item, e)) {
  // ......
}
// ......

我們來看一下tryMatch方法的內部邏輯:

/** Tries to CAS-match this node; if successful, wakes waiter. */
final boolean tryMatch(Object cmp, Object val) {
  // 使用原子操作設定當前Node對象的item屬性爲null
  // 如果設置成功,則通知Node對象中記錄的可能的waiter線程(等待匹配操作的線程)
  // 解除阻塞狀態。LockSupport工具類之前的文章已經花較大篇幅介紹了,這裏不再贅述
  if (casItem(cmp, val)) {
    LockSupport.unpark(waiter);
    return true;
  }
  return false;
}

一箇中心思想是,當p變量引用的Node節點對象成功調用tryMatch方法後,這個節點對象的isData屬性值和item屬性中實際的數據對象引用情況,就變得相悖——也就是說這個Node節點對象變成了一個“虛”節點。如下圖所示(編號642的Node節點對象變成了虛節點):
在這裏插入圖片描述
接下來,由於p != h的判定式成立,所以出隊邏輯會調用skipDeadNodesNearHead方法將h變量指向的節點(包含)和p變量指向的節點(包含)間的所有節點,作爲無效節點清除掉,並且重新設置LinkedTransferQueue隊列head屬性的引用位置。我們來看看skipDeadNodesNearHead方法內部是如何工作的:

// 該方法負責清理單向鏈表中的無效節點,既是isData屬性值和item屬性值相悖的那些節點
// h變量表示清理的開始(節點)位置
// p變量表示清理的結束(節點)位置,p所引用的Node節點一定是一個無效節點
private void skipDeadNodesNearHead(Node h, Node p) {
  // 循環的目的並不是cas原理,而是爲了找到單向鏈表中離鏈表頭部最近的有效節點
  for (;;) {
    final Node q;
    // 如果清理過程發現已經達到當前鏈表的最後一個節點,則p節點不能再“向後移動”了
    // 注意每次循環都會有一個變量q,指向當前p變量所指向Node節點對象的下一個Node節點對象
    if ((q = p.next) == null) {
      break;
    }
    // 如果q變量指向的Node節點是有效的,就說明已找到了單向鏈表中離鏈表頭部最近的有效節點了
    // 將q變量的值賦給p,以便達到“向後移動”的目的,並且不需要再繼續向後找了,推出循環
    else if (!q.isMatched()) { 
      p = q;
      break;
    }
    // 如果以上條件不成立,則還是要將q變量的值賦給p,而且通過循環,繼續向鏈表的後續結點尋找。
    // 注意:如果p節點出現了自循環的情況,這種情況代表p已經被其它線程的調用過程清理出了隊列,那麼直接退出處理即可
    else if (p == (p = q)) {
      return;
    }
  }
  
  // 當方法的以上操作成功找到自己認爲的最接近鏈表頭部的有效節點
  // 則通過原則操作,重新設置單向鏈表的head屬性的對象引用位置,並將原來h變量引用的Node節點設置爲自循環
  // 表示這個節點已經被移出隊列
  if (casHead(h, p)) {
    h.selfLink();
  }
}

// 該方法用於確認當前Node節點對象的isData屬性值和item屬性值是否相悖(是否有效)
// 所謂相悖,是指如下兩種情況中的一種:
// a、當isData屬性值爲true時,item屬性卻爲null
// b、當isData屬性值爲false時,item屬性卻不爲null 
// 如果兩個屬性的值相悖,則返回true
final boolean isMatched() {
  return isData == (item == null);
}

通過skipDeadNodesNearHead方法的調用,如果其中的cas操作成功,那麼單向鏈表呈現的狀態可用下圖進行表示:
在這裏插入圖片描述

2.4.3、xfer方法工作過程總結

上文中我們逐句閱讀了xfer中的源代碼,並通過一個典型的多生產者、多消費者的使用場景討論了LinkedTransferQueue隊列的工作過程。要說明的是,無論是上文中提到的生產者先工作然後消費者再工作;還是反向的場景:消費者先工作然後生成者再工作;又或者生產者和消費者一同工作,LinkedTransferQueue中單向鏈表的基本工作原理都相同。

如此,我們基本可以總結出LinkedTransferQueue內部單向鏈表工作的幾個特點:

  • 單向鏈表中並不是所有節點都有效(有“虛”節點存在),但除了“虛”節點以外,整個單向鏈表所有有效節點只可能是同一種任務模式——要麼全是取數任務,要麼全是存儲任務。

  • tail引用的位置不一定在單向鏈表的最末尾,這可能是因爲多線程下的併發操作導致的,還可能是在同一線程中兩次連續操作導致的。

  • head引用的位置也不一定在單向鏈表的頭部,這也是因爲多線程下的併發操作導致的。而且單向鏈表可以保證在head引用位置之前還沒有脫離單向鏈表的所有Node節點都是“虛”節點(無效節點)。

  • 而且基於以上兩個描述,我們還可以得出一個結論,就是head可能在某種情況下,會指向tail引用之後的Node節點(也就是head引用的位置在tail引用位置之後),如下圖所示:
    在這裏插入圖片描述
    這種情況是正常的,最直白的解釋就是:在多線程的操作場景下,出隊操作追趕上了入隊操作——或者說入隊操作還沒有來得急修正tail的引用位置,剛入隊的Node節點就被出隊了。

  • 當xfer方法中通過skipDeadNodesNearHead方法清理無效Node節點時,並不是直接將無效節點置爲null,而是將無效節點的next屬性引用向它自己,這樣做主要有兩個原因:

    • 原因1:讓無效Node節點失去引用路徑可達性,以便幫助垃圾回收器進行回收

    • 原因2:但是以上原因並不是最主要的原因,畢竟即使不將無效節點對象的next屬性引用指向它自己,無效Node節點也會因爲head引用位置後移而失去路徑可達性。這樣做的最主要原因是在多線程場景下,方便告知處理進度“落後於”自己的出隊處理線程,它們正在處理的Node節點已經被當前線程完成了出隊處理,已經變成了無效狀態,需要他們重新開始自己的出隊邏輯。這就是xfer方法中“p == (p = q) {continue restart;}” 語句的意義。

============(接下文)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章