併發隊列之ConcurrentLinkedQueue

来源:https://www.cnblogs.com/wyq1995/archive/2020/02/07/12271664.html
-Advertisement-
Play Games

本來想著直接說線程池的,不過在說線程池之前,我們必須要知道併發安全隊列;因為一般情況下線程池中的線程數量是一定的,肯定不會超過某個閾值,那麼當任務太多了的時候,我們必須把多餘的任務保存到併發安全隊列中,當線程池中的線程空閑下來了,就會到併發安全隊列中拿任務; 那麼什麼是併發安全隊列呢?其實可以簡單看 ...


  本來想著直接說線程池的,不過在說線程池之前,我們必須要知道併發安全隊列;因為一般情況下線程池中的線程數量是一定的,肯定不會超過某個閾值,那麼當任務太多了的時候,我們必須把多餘的任務保存到併發安全隊列中,當線程池中的線程空閑下來了,就會到併發安全隊列中拿任務;

  那麼什麼是併發安全隊列呢?其實可以簡單看作是一個鏈表,然後我們先辦法去存取節點;總的來說,併發安全隊列分為兩種,一種是阻塞的,一種是非阻塞的,前者是用鎖來實現的,後者用CAS實現的;

 

一.簡單介紹ConcurrentLinkedQueue

  這個隊列用法沒什麼好說的,就類似LinkedList的用法,怎麼對一個鏈表繼續增刪改查,不多說,我們就說一下其中幾個關鍵的方法;

  首先,這個隊列是一個線程安全的無界非阻塞隊列,其實就是一個單向鏈表,無界的意思就是沒有限制最大長度,非阻塞表示用CAS實現入隊和出隊操作,我們打開這個類就可以知道,有一個內部類Node,其中重要的屬性如下所示:

//用於存放節點的值
volatile E item;
//指向下一個節點
volatile Node<E> next;
//這裡也是用的是UNSAFE類,前面說過了,這個類直接提供CAS操作
private static final sun.misc.Unsafe UNSAFE;
//item欄位的偏移量
private static final long itemOffset;
//next的偏移量
private static final long nextOffset;

 

 

  然後ConcurrentLinkedQueue中幾個重要的屬性,好像也沒什麼重要的,就保存了頭節點和尾節點,註意,預設情況下頭節點和尾節點都是哨兵節點,也就是一個存null的Node節點

//存放鏈表的頭節點
private transient volatile Node<E> head;
//存放鏈表的尾節點
private transient volatile Node<E> tail;
//UNSAFE對象
private static final sun.misc.Unsafe UNSAFE;
//head欄位的偏移量
private static final long headOffset;
//tail欄位偏移量
private static final long tailOffset;

 

 

 

 

  下麵我們直接看一些重要方法吧!慢慢分析其中的演算法才是關鍵的

 

二.offer方法

  這個方法的作用就是在隊列末端添加一個節點,如果傳遞的參數是null,就拋出空指針異常,否則由於該隊列是無界隊列,該方法會一直返回true,而且該方法使用CAS演算法實現的,所以不會阻塞線程;

//隊列末端添加一個節點
public boolean offer(E e) {
    //如果e為空,那麼拋出空指針異常
    checkNotNull(e);
    //將傳進來的元素封裝成一個節點,Node的構造器中調用UNSAFE.putObject(this, itemOffset, item)把e賦值給節點中的item
    final Node<E> newNode = new Node<E>(e);

    //[1]
    //這裡的for迴圈從最後的節點開始
    for (Node<E> t = tail, p = t;;) {
      Node<E> q = p.next;
      //[2]如果q為null,說明p就是最後的節點了
        if (q == null) {
            //[3]CAS更新:如果p節點的下一個節點是null,就把寫個節點更新為newNode
            if (p.casNext(null, newNode)) {
                //[4]CAS成功,但是這時p==t,所以不會進入到這裡的if裡面,直接返回true
                //那麼什麼時候會走到這裡面來呢?其實是要有另外一個線程也在調用offer方法的時候,會進入到這裡面來
                if (p != t) 
                    casTail(t, newNode);  
                return true;
            }
        }
        else if (p == q) //[5]
            
            p = (t != (t = tail)) ? t : head;
        else //[6]
            p = (p != t && t != (t = tail)) ? t : q;
    }
}

 

  

  上面執行到[3]的時候,由於頭節點和尾節點預設都是指向哨兵節點的,由於這個時候p的下一個節點為null,所以當前線程A執行CAS會成功,下圖所示;

 

 

  如果此時還有一個線程B也來嘗試[3]中CAS,由於此時p節點的下一個節點不是null了,於是線程B會跳到[1]出進行第二次迴圈,然後會到[6]中,由於p和t此時是相等的,所以這裡是false,即p=q,下圖所示:

 

 

  然後線程B又會跳到[1]處進行第三次迴圈,由於執行了Node<E> q = p.next,所以此時q指向最後的null,就到了[3]處進行CAS,這次是可以成功的,成功之後如下圖所示:

 

 

   

  這個時候因為p!=t,所以可以進入到[4],這裡又會進行一個CAS:如果tail和t指向的節點一樣,那麼就將tail指向新添加的節點,如圖所示,這個時候線程B也就執行完了;

 

   

  其實還有[5]沒有走到,這個是在poll操作之後才執行的,我們先跳過,等說完poll方法之後再回頭看看;另外說一下,add方法其實就是調用的是offer方法,就不多說了;

 

 

三.poll方法

  這個方法是獲取頭部的這個節點,如果隊列為空則返回null;

public E poll() {
    //這裡其實就是一個goto的標記,用於跳出for迴圈
    restartFromHead:
    //[1]
    for (;;) {
        for (Node<E> h = head, p = h, q;;) {
            E item = p.item;
            //[2]如果當前節點中存的值不為空,則CAS設置為null
            if (item != null && p.casItem(item, null)) {
                //[3]CAS成功就更新頭節點的位置
                if (p != h) 
                    updateHead(h, ((q = p.next) != null) ? q : p);
                return item;
            }
            //[4]當前隊列為空,就返回null
            else if ((q = p.next) == null) {
                updateHead(h, p);
                return null;
            }
            //[5]當前節點和下一個節點一樣,說明節點自引用,則重新找頭節點
            else if (p == q)
                continue restartFromHead;
            //[6]
            else
                p = q;
        }
    }
}

final void updateHead(Node<E> h, Node<E> p) {
    if (h != p && casHead(h, p))
        h.lazySetNext(h);
}

  

  分為幾種情況,第一種情況是線程A調用poll方法的時候,發現隊列是空的,即頭節點和尾節點都指向哨兵節點,就會直接到[4],返回null

  第二種情況,線程A執行到了[4],此時有一個線程卻調用offer方法添加了一個節點,下圖所示,那麼此時線程A就不會走[4]了,[5]也不滿足,於是會到[6]這裡來,然後線程A又會跳到[1]處進行迴圈,此時p指向的節點中item不為null,所以會到[2]中;

 

   

  到了[2]中將p指向的節點中item用CAS設置為null,然後就到了[3],下麵第一個圖,由於p!=h,q=null,所以最後調用的是updateHead(h,p),這方法:如果頭節點和h指向的是一樣的,就將頭節點指向p,我們還能看到updateHead方法中h.lazySetNext(h)表示h的下一個節點指向自己,下麵圖二

 

   到了這裡還沒完,還記不記得offer方法中有一個地方的代碼沒有執行的啊!就是這種情況,尾節點自己引用自己,我們再調用offer會怎麼樣呢?

  回到offer方法,先會到[1],然後q指向自己這個哨兵節點(註意,此時雖然p指向的節點中存的是null,但是p!=null},於是再到[5],此時的圖如下左圖所示;此時由於t==tail,所以p=head;

 

   再在offer方法迴圈一次,此時q指向null,下麵左圖所示,然後就可以進入[2]中進行CAS,CAS成功,因為此時p!=t,所以還要進行CAS將tail指向新節點,下麵右圖所示,可以讓GC回收那個垃圾!

媽耶,這裡比較繞!哈哈哈哈哈哈哈哈哈哈哈

 

 

 

四.peek方法

  這個方法的作用就是獲取隊列頭部的元素,只獲取不移除,註意這個方法和上面的poll方法的區別啊!

public E peek() {
    //[1]goto標誌
    restartFromHead:
    for (;;) {
        for (Node<E> h = head, p = h, q;;) {
            //[2]
            E item = p.item;
            //[3]
            if (item != null || (q = p.next) == null) {
                updateHead(h, p);
                return item;
            }
            //[4]
            else if (p == q)
                continue restartFromHead;
            else//[5]
                p = q;
        }
    }
}

  final void updateHead(Node<E> h, Node<E> p) {
    if (h != p && casHead(h, p))
        h.lazySetNext(h);
  }

 

 

  

  如果隊列中為空的時候,走到[3]的時候,就會如下圖所示,由於h==p,所以updateHead方法啥也不做,然後返回就返回item為null

 

 

  如果隊列不為空,那麼如下左圖所示,此時進入迴圈內不滿足條件,會到[5]這裡,將p指向q,然後再進行一次迴圈到[3],將q指向p的後一個節點,下麵右圖所示;

 

 

  然後調用updateHead方法,用CAS將頭節點指向p這裡,然後將h自己指向自己,下圖所示,最後返回item

 

 

五.總結

  其實還有幾個方法沒說,但是感覺比較容易就不浪費篇幅了,有興趣的可以看看:size方法用於計算隊列中節點的數量,可是由於沒有加鎖,在併發的條件下不准確;remove方法刪除某個節點,其實就是遍歷然後用equals方法比較item是不是一樣,只不過如果存在多個符合條件的節點只刪除第一個,然後返回true,否則返回false;contains方法判斷隊列中是否包含指定item的節點,也就是遍歷,很容易;

  最麻煩的就是offer方法和poll方法,offer方法是在隊列的最後面添加節點,而poll是獲取頭節點,並且刪除第一個真正的隊列節點(註意,節點分為兩種,一種是哨兵節點,一種是真正的存了數據的節點啊),還簡單的說了一下poll方法和peek方法的區別,後者只是獲取,而不刪除啊!用下麵這個圖幫助記憶一下;

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ``` 你把函數定義在 Vue 的原型鏈上,只能在 Vue 的實例里才能取到這個方法。 vue組件 是一個Vue 的實例,所以你當然能在這裡調用到 ajax 方法。 而,vuex 只是一個 vue插件,在 vuex 里的 this 不是指向 vue實例的,所以肯定是取不到 ajax 方法的。 建議:... ...
  • vue cli3 一直運行 /sockjs node/info?t= 解決方案 首先 sockjs node 是一個JavaScript庫,提供跨瀏覽器JavaScript的API,創建了一個低延遲、全雙工的瀏覽器和web伺服器之間通信通道。 服務端:sockjs node(https://gith ...
  • 當需要從vuex中獲取的變數特別多是,可以使用mapState代替 當一個組件需要獲取多個狀態時候,將這些狀態都聲明為計算屬性會有些重覆和冗餘。為瞭解決這個問題,我們可以使用 mapState 輔助函數幫助我們生成計算屬性,讓你少按幾次鍵: // 在單獨構建的版本中輔助函數為 Vuex.mapSta ...
  • axios 基於http客戶端的promise,面向瀏覽器和nodejs 特色 瀏覽器端發起XMLHttpRequests請求 node端發起http請求 支持Promise API 監聽請求和返回 轉化請求和返回 取消請求 自動轉化json數據 客戶端支持抵禦 安裝 使用npm: npm inst ...
  • |這個作業屬於哪個課程|軟體工程| | | | |這個作業要求在哪裡|第一次個人編程作業| |這個作業的目標|完成漢字編程| |作業正文|見下文 | |其他參考文獻|無,但是感謝洪成龍與陳徳渠的解答 | 編程信息 時間:2020.02.06|2020.02.07 代碼行數:86行|338行 需求分析 ...
  • 前言:之前打 CTF 的時候都是零零碎碎的學習Python,沒有成體系,學得不精。趁著過年的這段時間好好地系統學習一下,加強自己的python技能。同時也做一個記錄,用來總結和反思,如果能給後學者一點幫助,那就再好不過了。 [TOC] 一、Python的下載 1. 到Python的 "官網" 上看適 ...
  • 1.CS和BS CS:Client/Server 客戶端和伺服器,這種軟體往往需要安裝。比如QQ、迅雷、播放器。 優點 : 可以減輕伺服器端壓力,將部分代碼寫到客戶端,並且界面很美觀。 缺點 : 一旦伺服器更新了,客戶端也需要更新,分散式開發比較弱。 BS:Browser/Server 瀏覽器和服務 ...
  • 今日內容 裝飾器 推導式 模塊【可選】 內容回顧 1. 函數 參數 def (a1,a2):pass def (a1,a2=None):pass 預設參數推薦用不可變類型,慎用可變類型。 def( args, kwargs):pass 註意:位置參數 關鍵字參數 面試題 函數可以做參數【知識點】。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...