讀懂操作系統之緩存原理(cache)(三)

前言

本節內容計劃是講解TLB與高速緩存的關係,但是在涉及高速緩的前提是我們必須要了解操作系統緩存原理,所以提前先詳細了解下緩存原理,我們依然是採取循序漸進的方式來解答緩存原理,若有敘述不當之處,還請批評指正。

緩存原理

高速緩存被劃分為多個塊,其大小可能不同,緩存中的塊數通常為2的冪。如下為一個具有八個塊的高速緩存,每個塊包含一個字節。

通過本節對緩存原理的學習我們能夠學習到四點:

【1】當我們將數據塊從主存儲器複製到緩存,我們到底應該放在哪裡?

【2】如何判斷一個字是否已經在緩存中,或者它是否必須首先從主存儲器中獲取?

【3】較小的緩存最終會填滿, 需要至從主存加載新塊,我們必須替換緩存中現有的哪個塊?

【4】存儲系統如何處理寫操作?

數據放在緩存哪裡?

緩存最簡單的數據結構是直接映射: 其中每個存儲器地址僅僅對應到緩存中的一個位置。例如如下,16個字節的主存和4個字節的緩存(每個塊一個字節),內存地址為0、4、8、12分別映射到緩存中為0的塊,而地址1、5、9、13被映射到塊1

等等,我們是不是講解的太快了,上述地址怎麼就劃分到比如塊0或塊1了呢?要找出緩存所在塊採取取模法:(塊地址)mod (緩存中的塊數),如果緩存包含2k塊,則內存地址i處的數據將進入緩存塊索引為i mod 2k。還是不懂?我們來舉個例子,如下緩存有4個塊,那麼地址為14將映射到塊2即(14 mod 4 = 2)。

為便於大家理解如上為10進製表示內存地址,將內存地址映射到緩存塊中實際等效的方式是將內存地址中的最低有效k位(二進制)進行映射。正如下面我們所看到的,內存地址14(1110,二進制)將最低有效位10作為塊中的索引

怎樣找到緩存中數據?

到目前為止我們知道了將地址利用直接映射的結構映射到緩存中,那麼我們找到數據是否在緩存中呢?如果要讀取內存地址i,則可以使用mod技巧來確定哪個緩存塊將包含i,如上所述,若其他地址也可能映射到相同的緩存塊,那麼我們如何區分它們呢?例如如下內存地址2、6、10、14都在緩存塊2中

為了解決這個問題,我們需要向高速緩存中添加標記(tag),通過內存地址的高位來提供標記位,以使我們能夠區分映射到同一高速緩存塊的不同存儲位置。例如如下。內存地址6即(0110,二進制),將低位10作為索引(index),高位01作為標記(tag)。

我們通過將高速緩存塊標記(tag)與塊索引(index)組合起來,可以準確地知道主存儲器的哪些地址存儲在高速緩存中。

當程序加載到內存中時,緩存為空,不包含有效數據,我們應該通過為每個緩存塊添加一個有效位來解決這個問題,系統初始化時,所有有效位均設置為0,當數據加載到特定的緩存塊中時,相應的有效位設置為1。

當CPU嘗試從內存中讀取數據時,該地址將被發送到緩存控制器,地址的最低k位將在緩存中索引一個塊,如果該塊有效且標籤與m位地址的高(m-k)位匹配,則該數據將被發送到CPU,如下為一個32位內存地址和210字節高速緩存的圖。

到這裏我們會發現一個問題,將每一個字節對應一字節緩存塊並沒有很好的利用空間局部性,要是訪問一個地址后將訪問附近的地址,我們又該怎麼辦?我們要做的是將緩存塊的大小要大於1個字節。如下,我們使用兩個字節的塊,因此我們可以用兩個來加載緩存一次讀取一個字節,如果我們從內存地址12讀取數據,則地址中的數據12和13都將被複制到緩存塊2。

現在,我們又該如何確定數據應放在緩存中的位置?現在演變成塊地址,如果緩存塊大小為2n字節,我們也可以在概念上將主內存也劃分成2n字節塊,要確定字節地址i的塊地址,可以進行整數除法(i / 2n),如下示例中有2個字節的緩存塊,因此我們可以將16個字節的主存儲器視為8塊主存儲器,例如,存儲器地址12和13都對應於塊地址6,因為12 / 2 = 6和13 / 2 = 6。 

現在我們知道了塊地址,就可以像上述一樣將其映射到緩存:找到塊地址除以緩存塊數后的餘數。在如下示例中,內存塊6屬於緩存塊2,因為6 mod 4 =2,這對應於將來自存儲器字節地址12和13的數據都放入高速緩存塊2中。

當我們訪問內存中的一個字節數據時,我們會將其整個塊複製到緩存中以達到充分利用空間局部性。在我們的示例中,如果程序從字節地址12讀取,我們會將所有存儲塊6(地址12和13)都加載到緩存塊2中(注意:字節地址13對應於相同的存儲塊地址)因此,對地址13的讀取也會導致將存儲塊6(地址12和13)加載到高速緩存塊2中。為了簡化起見,存儲塊的字節i始終存儲在相應高速緩存塊的字節i中。

假設我們有一個包含2k塊的緩存,每個塊包含2n個字節,我們可以通過查看其在主內存中的地址來確定該緩存中一個字節的數據位置,地址的k位將選擇2k個高速緩存塊之一,最低的n位現在是一個塊偏移量,它決定了高速緩存塊中的2n個字節中的哪個將存儲數據。

我們來舉個例子加深理解,如下示例使用22塊高速緩存,每個塊佔21字節,因此,存儲器地址13(1101)將存儲在高速緩存塊2的字節1中。

到這裏為止,我們才算分析清楚了緩存中有效位、標記位、索引、偏移它們的由來以及實際作用。同時對於緩存採用的直接映射(direct mapped)結構:索引和偏移量可以使用位運算符或簡單的算術運算,因為每個內存地址都恰好屬於一個塊。實際上我們可以將一個塊放置到緩存中的任何一個位置,這種機制稱為全相聯(fully associative)。全相聯的高速緩存允許將數據存儲在任何高速緩存塊中,而不是將每個內存地址強制映射到一個特定的塊中,從內存中獲取數據時,可以將其放置在高速緩存的任何未使用塊中。 這樣,我們將永遠不會在映射到單個緩存塊的兩個或多個內存地址之間發生衝突,在上述示例中,我們可能將內存地址2放在緩存塊2中,並將地址6放在塊3中。然後對2和6的後續重複訪問將全部命中而不是未命中,如果所有塊都已被使用,則使用LRU算法進行替換。但是在全相聯緩存中要查找一個指定的塊,由於該塊存放在緩存中的任何位置,因此需要檢索緩存中的所有項,為了是檢索更加有效,它是由一個與緩存中每個項都相關的比較器并行完成的,這些比較器加大了硬件開銷,因而,全相聯只適合塊數較少的緩存。介於直接映射和全相聯之間的設計是組相聯(set associative)。在組相聯緩存中,每個塊可被放置的位置數固定,每個塊有n個位置可放的緩存被稱作n路組相聯,一個n路組相聯緩存由很多組組成,每個組有n個塊。通過上述對直接映射的講解,最終我們得出指定內存地址所在存儲的塊號為:(塊號) mod (緩存中的塊數),而組相聯對於存儲塊號是:(塊號) mod (緩存中的組數)。如下為8塊高速緩存的組織

組相聯實際上就是將塊進行分組,比如如上第一個圖則是直接映射(我們大可將其看做是每一個塊就是一個組,所以是1路8組相聯),而第二張圖則是每2個塊作為一組,所以是2路4組相聯,同理第三張圖是4路2組相聯。換句話說,若每組有2n塊,那麼就是2n路相聯。通過對組相聯的講解,我們再敘內存地址在組相聯緩存中的位置。如果我們有2s組並且每塊有2n字節,那麼內存地址映射在緩存中的位置則是如下這般

現在我們運算則是計算緩存中的組索引而非再是塊,上述Block offset(在組中塊偏移)= 內存地址 mod 2n,塊地址 = 內存地址 / 2n,set index(組索引) = 塊地址 mod 2s。我們還是通過圖解來進行敘述,假設有一個8塊的高速緩存,然後每個塊是16個字節,那麼內存地址為6195的數據存儲在緩存哪裡呢?首先我們將6195轉換為二進制  = 110000 011 0011,因每個塊是16字節即24,所以塊偏移量為4位即0011,若採用1路8組相聯(直接映射)那麼其組索引就是(6195 mod 8) = 3,取6195轉換而二進制去除偏移量4位,所以為011,同理(根據上述給定計算公式)對於2路4組相聯其組索引為(11),4路2組相聯其組索引為(1),如下:

到這裏我們知道將數據進行緩存我們可以採取直接映射、組相聯、全相聯的機制,通過增加相聯度通常可以降低緩存缺失率,但是增加相聯度也就增加了每組中的塊數,也就是并行查找時同時比較的次數,相聯度每增加兩倍就會使得每組中的塊數加倍而使得組數減半,所以增大了訪問時間的開銷。如何找到一個塊,當然也就依賴於所使用的將塊放置的機制(直接映射、組相聯、全相聯),相較於全相聯而言,它使用複雜的替換策略而降低缺失率且很容易被索引,而不需要額外的硬件,也不需要進行查找。因此虛擬存儲系統通常使用全相聯映射,而組相聯映射通常應用於緩存和TLB。

緩存缺失和寫操作

緩存缺失被分為以下三類(3C模型,three Cs model),因其三類名稱以字母c開頭而得名

強制缺失(compulsory miss):對從沒有在緩存中出現的塊第一次進行訪問引起的缺失,也稱為冷啟動缺失(cold-start miss)

容量缺失:(capacity miss):由於緩存容納不了一個程序執行所需要的所有塊而引起的緩存缺失,當某些塊被替換出去,隨後再被調入時,將發生容量缺失

衝突缺失(conflict miss):在組相聯或者直接映射的緩存中,多個競爭同一個組時而引起的緩存缺失。衝突缺失在直接映射或組相聯緩存中存在,而在同樣大小的全相聯緩存中不存在,這種緩存缺失也稱為碰撞缺失(collision miss)

改變緩存設計的某一方面就能直接影響這些缺失的原因。衝突缺失是因為爭用同一個緩存塊而引起的,因此提高相聯度可以減少衝突缺失,然後提高相聯度會延長訪問時間,導致整個性能的降低,容量缺失可以簡單地通過增大緩存容量來減少,當然緩存容量增大的同時必然導致訪問時間的增加,也將導致整體性能的降低。

在相聯的緩存中發生缺失時,我們必須決定替換哪一塊,如若是全相聯,那麼所有的塊都是被替換的候選者,如若是組相聯,我們必須在某一組的塊中進行選擇,當然,直接映射的緩存替換很簡單,因為只有一個可以替換的候選者。因此在全相聯或組相聯緩存中 ,有兩種主要的替換策略

隨機法:隨機選擇候選塊,可能使用一些硬件來協助實現,例如TLB缺失、MIPS支持隨機替換

LRU(最近最少使用算法):被替換的塊是最久沒有被使用過的塊 (在大多虛擬存儲器中,對於LRU都是通過提供引用位來近似實現(比如TLB))

指令緩存缺失(數據缺失也類似如此)處理步驟如下:

【1】將程序計數器(PC)的原始值送到寄存器

【2】通知主存執行一次讀操作,並等待主存訪問完成

【3】寫緩存項,將從主存取回的數據寫入緩存中存放數據的部分,並將高位(從ALU中得到)寫入標記域,設置有效位

【4】重啟指令執行第一步,重新取指,這次該指令發生在緩存中

數據訪問是對緩存的控制基本相同:發生缺失時,處理器發生阻塞,直到從存儲器中取回數據后才響應。在執行寫操作時,如果有一個存儲指令,我們只將數據寫入緩存而不改變主存中的內容,那麼在寫入緩存后將導致緩存和主存被認為不一致,保持主存和緩存一致性最簡單的方法是將數據同時寫入主存和緩存中,這種方法稱為【寫直達】法。但是這種方法無法提供良好的性能,因為每次寫操作都要把數據寫入主存中,這些寫操作將花費大量的時間,可能至少花費100個處理時鐘周期,並且大大降低了機器速度,解決這個問題的方案之一是採用【寫緩衝:一個保存等待寫入主存數據的緩衝隊列】,當一個數據在等待寫入緩存時,先將其寫入緩衝中,當數據寫入緩存和緩衝后,處理器可以繼續執行,當寫主存操作完成后,寫緩衝里的數據項也得到有效釋放。如果寫緩衝已經滿了,那麼當處理器執行到一個寫操作時就必須停下來直到寫緩衝中有一個空位置,當然,如果存儲器完成寫操作的速度比處理器產生寫操作的速度慢,那麼再多的緩衝器也無用,因為產生寫操作比存儲系統接收它們更快。

 

除了寫直達方法外,另外一種可選擇的方法是【寫回】,在寫回機制中,當發生寫操作時,新值僅僅被寫入到緩存塊中,只有當修改過的塊被替換時才需要寫到磁盤上,寫回機制可提高系統性能,尤其是當處理器寫操作的速度和主存處理寫操作速度一樣快甚至更快時,但是,寫回機制的實現比寫直達要複雜得多。大部分寫回機制的緩衝也是使用寫緩衝,當在發生缺失替換一個被修改的塊時,寫緩衝可以起到降低缺失代價的作用。在這種情況下,被修改的數據塊移入與緩存相聯的寫回緩衝器,同時從主存中讀出所需要的數據塊。隨後,寫回緩衝器再將數據寫入主存,如果下一次缺失沒有立刻發生,當臟數據塊必須被替換時,這種方法可以減少一半的缺失代價。

總結

一個緩存塊可以放在何處:一個位置(直接映射),一些位置(組相聯),任何位置(全相聯)。

如何找到一個塊:索引(直接映射的緩存中),有限的檢索(組相聯的緩存中),全部檢索(全相聯的緩存中)、專用查找頁表。

緩存缺失時替換哪一塊:隨機選取、LRU

寫操作如何處理:寫直達或寫回策略

本文我們非常詳細的講解了緩存的基本原理,當然對於如何處理緩存一致性並未涉及(大多採用監聽協議),希望通過我對緩存原理的理解能給閱讀的您能有力所能及的幫助,謝謝。 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

※別再煩惱如何寫文案,掌握八大原則!

曹工說JDK源碼(2)–ConcurrentHashMap的多線程擴容,說白了,就是分段取任務

前言

先預先說明,我這邊jdk的代碼版本為1.8.0_11,同時,因為我直接在本地jdk源碼上進行了部分修改、調試,所以,導致大家看到的我這邊貼的代碼,和大家的不太一樣。

不過,我對源碼進行修改、重構時,會保證和原始代碼的功能、邏輯嚴格一致,更多時候,可能只是修改變量名,方便理解。

大家也知道,jdk代碼寫得實在是比較深奧,變量名經常都是單字符,i,j,k啥的,實在是很難理解,所以,我一般會根據自己的理解,去重命名,為了減輕我們的頭腦負擔。

至於怎麼去修改代碼並調試,可以參考我之前的文章:

曹工力薦:調試 jdk 中 rt.jar 包部分的源碼(可自由增加註釋,修改代碼並debug)

文章中,我改過的代碼放在:

https://gitee.com/ckl111/jdk-debug

sizeCtl field的初始化

大家知道,concurrentHashMap底層是數組+鏈表+紅黑樹,數組的長度假設為n,在hashmap初始化的時候,這個n除了作為數組長度,還會作為另一個關鍵field的值。

    /**
     * Table initialization and resizing control.  When negative, the
     * table is being initialized or resized: -1 for initialization,
     * else -(1 + the number of active resizing threads).  Otherwise,
     * when table is null, holds the initial table size to use upon
     * creation, or 0 for default. After initialization, holds the
     * next element count value upon which to resize the table.
     */
    private transient volatile int sizeCtl;

該字段非常關鍵,根據取值不同,有不同的功能。

使用默認構造函數時

    public ConcurrentHashMap() {
    }

此時,sizeCtl被初始化為0.

使用帶初始容量的構造函數時

此時,sizeCtl也是32,和容量一致。

使用另一個map來初始化時

    public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
        this.sizeCtl = DEFAULT_CAPACITY;
        putAll(m);
    }

此時,sizeCtl,直接使用了默認值,16.

使用初始容量、負載因子來初始化時

    public ConcurrentHashMap(int initialCapacity, float loadFactor) {
        this(initialCapacity, loadFactor, 1);
    }

這裏重載了:

這裏,我們傳入的負載因子為0.75,這也是默認的負載因子,傳入的初始容量為14.

這裏面會根據: 1 + 14/0.75 = 19,拿到真正的size,然後根據size,獲取到第一個大於19的2的n次方,即32,來作為數組容量,然後sizeCtl也被設置為32.

initTable時,對sizeCtl field的修改

實際上,new一個hashmap的時候,我們並沒有創建支撐數組,那,什麼時候創建數組呢?是在真正往裡面放數據的時候,比如put的時候。

/** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());

        int binCount = 0;
        ConcurrentHashMapPutResultVO vo = new ConcurrentHashMapPutResultVO();
        vo.setBinCount(0);
        for (Node<K,V>[] tab = table;;) {
            int tableLength;
            // 1
            if (tab == null) {
                tab = initTable();
                continue;
            }
            ...
        }

1處,即會去初始化table。

/**
     * Initializes table, using the size recorded in sizeCtl.
     * 初始化hashmap,使用sizeCtl作為容量
     */
    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            sc = sizeCtl;
            if (sc < 0){
                Thread.yield(); // lost initialization race; just spin
                continue;
            }

            /**
             * 走到這裏,說明sizeCtl大於0,大於0,代表什麼,可以去看下其構造函數,此時,sizeCtl表示
             * capacity的大小。
             * {@link #ConcurrentHashMap(int)}
             *
             * cas修改為-1,如果成功修改為-1,則表示搶到了鎖,可以進行初始化
             *
             */
            // 1
            boolean bGotChanceToInit = U.compareAndSwapInt(this, SIZECTL, sc, -1);
            if (bGotChanceToInit) {
                try {
                    tab = table;
                    /**
                     * 如果當前表為空,尚未初始化,則進行初始化,分配空間
                     */
                    if (tab == null || tab.length == 0) {
                        /**
                         * sc大於0,則以sc為準,否則使用默認的容量
                         */
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;

                        Node<K, V>[] nt = (Node<K, V>[]) new Node<?, ?>[n];
                        table = tab = nt;
                        /**
                         * n >>> 2,無符號右移2位,則是n的四分之一。
                         * n- n/4,結果為3/4 * n
                         * 則,這裏修改sc為 3/4 * n
                         * 比如,默認容量為16,則修改sc為12
                         */
                        // 2
                        sc = n - (n >>> 2);
                    }
                } finally {
                    /**
                     * 修改sizeCtl到field
                     */
                    // 3
                    sizeCtl = sc;
                }
                break;
            }
        }

        return tab;
    }
  • 1處,cas修改sizeCtl為-1,成功了的,獲得初始化table的權利
  • 2處,修改局部變量sc為: n – (n >>> 2),也就是修改為 0.75n,假設此時的數組容量為16,那麼sc就是16 * 0.75 = 12.
  • 3處,將sc賦值到field: sizeCtl

經過上面的分析,initTable時,這個字段可能有兩種取值:

  • -1,有線程正在對該table進行初始化
  • 0.75*數組長度,此時,已經初始化完成

上面說的是,在put的時候去initTable,實際上,這個initTable,也會在以下函數中被調用,其共同點就是,都是往裡面放數據的操作:

擴容時機

上面說了很多,目前,我們知道的是,在initTable后,sizeCtl的值,是舊的數組的長度 * 0.75。

接下來,我們看看擴容時機,在put時,會調用putVal,這個函數的大體步驟:

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    // 1
    int hash = spread(key.hashCode());

    int binCount = 0;
    System.out.println("binCount:" + binCount);
    // 2
    ConcurrentHashMapPutResultVO vo = new ConcurrentHashMapPutResultVO();
    vo.setBinCount(0);
    for (Node<K,V>[] tab = table;;) {
        int tableLength;
        // 3
        if (tab == null) {
            tab = initTable();
            continue;
        }
        
        tableLength = tab.length;
        if (tableLength == 0) {
            tab = initTable();
            continue;
        }

        int entryNodeHashCode;
		
        // 4
        int entryNodeIndex = (tableLength - 1) & hash;
        Node<K,V> entryNode = tabAt(tab,entryNodeIndex);

        /**
         * 5 如果我們要放的桶,還是個空的,則直接cas放進去
         */
        if (entryNode == null) {
            Node<K, V> node = new Node<>(hash, key, value, null);

            // no lock when adding to empty bin
            boolean bSuccess = casTabAt(tab, entryNodeIndex, null, node);
            if (bSuccess) {
                break;
            } else {
                /**
                 * 如果沒成功,則繼續下一輪循環
                 */
                continue;
            }
        }
		
        entryNodeHashCode = entryNode.hash;
        /**
         * 6 如果要放的這個桶,正在遷移,則幫助遷移
         */
        if (entryNodeHashCode == MOVED){
            tab = helpTransfer(tab, entryNode);
            continue;
        }


        /**
         * 7 對entryNode加鎖
         */
        V oldVal = null;
        System.out.println("sync");
        synchronized (entryNode) {
            /**
             * 這一行是判斷,在我們執行前面的一堆方法的時候,看看entryNodeIndex處的node是否變化
             */
            if (tabAt(tab, entryNodeIndex) != entryNode) {
                continue;
            }

            /**
             * 8 hashCode大於0,說明不是處於遷移狀態
             */
            if (entryNodeHashCode >= 0) {
                /**
                 * 9 鏈表中找到合適的位置並放入
                 */
                findPositionAndPut(key, value, onlyIfAbsent, hash, vo, entryNode);
                binCount = vo.getBinCount();
                oldVal = (V) vo.getOldValue();
            }
            else if (entryNode instanceof TreeBin) {
                ...
            }
        }
		
        System.out.println("binCount:" + binCount);
        // 10
        if (binCount != 0) {
            if (binCount >= TREEIFY_THRESHOLD)
                treeifyBin(tab, entryNodeIndex);
            if (oldVal != null)
                return oldVal;
            break;
        }
    }
    // 11
    addCount(1L, binCount);
    return null;
}
  • 1處,計算key的hashcode

  • 2處,我這邊new了一個對象,裏面兩個字段:

    public class ConcurrentHashMapPutResultVO<V> {
        int binCount;
    
        V oldValue;
    }
    

    其中,oldValue用來存放,如果put進去的key/value,其中key已經存在的話,一般會直接覆蓋之前的舊值,這裏主要存放之前的舊值,因為我們需要返回舊值。

    binCount,則存放:在找到對應的hash桶之後,在鏈表中,遍歷了多少個元素,該值後面會使用,作為一個標誌,當該標誌大於0的時候,才去進一步檢查,看看是否擴容。

  • 3處,如果table為null,說明table里沒有任何一個鍵值對,數組也還沒創建,則初始化table

  • 4處,根據hashcode,和(數組長度 – 1)相與,計算出應該存放的哈希桶在數組中的索引

  • 5處,如果要放的哈希桶,還是空的,則直接cas設置進去,成功則跳出循環,否則重試

  • 6處,如果要放的這個桶,該節點的hashcode為MOVED(一個常量,值為-1),說明有其他線程正在擴容該hashmap,則幫助擴容

  • 7處,對要存放的hash桶的頭節點加鎖

  • 8處,如果頭節點的hashcode大於0,說明是拉了一條鏈表,則調用子方法(我這邊自己抽的),去找到合適的位置並插入到鏈表

  • 9處,findPositionAndPut,在鏈表中,找到合適的位置,並插入

  • 10處,在findPositionAndPut函數中,會返回:為了找到合適的位置,遍歷了多少個元素,這個值,就是binCount。

    如果這個binCount大於8,則說明遍歷了8個元素,則需要轉紅黑樹了。

  • 11處,因為我們新增了一個元素,總數自然要加1,這裏面會去增加總數,和檢查是否需要擴容。

其中,第9步,因為是自己抽的函數,所以這裏貼出來給大家看下:

/**
     * 遍歷鏈表,找到應該放的位置;如果遍歷完了還沒找到,則放到最後
     * @param key
     * @param value
     * @param onlyIfAbsent
     * @param hash
     * @param vo
     * @param entryNode
     */
    private void findPositionAndPut(K key, V value, boolean onlyIfAbsent, int hash, ConcurrentHashMapPutResultVO vo, Node<K, V> entryNode) {
        vo.setBinCount(1);

        for (Node<K,V> currentIterateNode = entryNode;
                ;
             vo.setBinCount(vo.getBinCount() + 1)) {


            /**
             * 如果當前遍歷指向的節點的hash值,與參數中的key的hash值相等,則,
             * 繼續判斷
             */
            K currentIterateNodeKey = currentIterateNode.key;
            boolean bKeyEqualOrNot = Objects.equals(currentIterateNodeKey, key);
            /**
             * key的hash值相等,且equals比較也相等,則就是我們要找的
             */
            if (currentIterateNode.hash == hash && bKeyEqualOrNot) {
                /**
                 * 獲取舊的值
                 */
                vo.setOldValue(currentIterateNode.val);

                /**
                 * 覆蓋舊的node的val
                 */
                if (!onlyIfAbsent)
                    currentIterateNode.val = value;
                // 這裏直接break跳出循環
                break;
            }

            /**
             * 把當前節點保存起來
             */
            Node<K,V> pred = currentIterateNode;
            /**
             * 獲取下一個節點
             */
            currentIterateNode = currentIterateNode.next;
            /**
             * 如果下一個節點為null,說明當前已經是鏈表的最後一個node了
             */
            if ( currentIterateNode  == null) {
                /**
                 * 則在當前節點後面,掛上新的節點
                 */
                pred.next = new Node<K,V>(hash, key,
                        value, null);
                break;
            }
        }

    }

第11步,也是我們要看的重點:

private final void addCount(long delta, int check) {
        CounterCell[] counterCellsArray = counterCells;
		// 1
        long b = baseCount;
    	// 2
        long newBaseCount = b + delta;

        /**
         * 3 直接cas在baseCount上增加
         */
        boolean bSuccess = U.compareAndSwapLong(this, BASECOUNT, b, newBaseCount);
        if ( counterCellsArray != null ||  !bSuccess) {
			...
            newBaseCount = sumCount();
        }
		
    	// 4
        if (check >= 0) {
            while (true) {

                Node<K,V>[] tab = table;
                Node<K,V>[] nt;
                int n = 0;
                // 5
                int sc =  sizeCtl;
                // 6
                boolean bSumExteedSizeControl = newBaseCount >= (long) sc;
				// 7
                boolean bContinue = bSumExteedSizeControl && tab != null && (n = tab.length) < MAXIMUM_CAPACITY;
                if (bContinue) {
                    int rs = resizeStamp(n);
                    if (sc < 0) {
                        if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                                sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                                transferIndex <= 0)
                            break;
                        if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                            transfer(tab, nt);
                    } else if (U.compareAndSwapInt(this, SIZECTL, sc,
                            (rs << RESIZE_STAMP_SHIFT) + 2))
                        // 8
                        transfer(tab, null);
                    newBaseCount = sumCount();
                } else {
                    break;
                }
            }

        }
    }
  • 1處,baseCount是一個field,存儲當前hashmap中,有多少個鍵值對,你put一次,就一個;remove一次,就減一個。

  • 2處,b + delta,其中,b就是baseCount,是舊的數量;dalta,我們傳入的是1,就是要增加的元素數量

    所以,b + delta,得到的,就是經過這次put后,預期的數量

  • 3處,直接cas,修改baseCount這個field為 新值,也就是第二步拿到的值。

  • 4處,這裏檢查check是否大於0,check,是第二個形參;這個參數,我們外邊怎麼傳的?

    addCount(1L, binCount);

    不就是bincount嗎,也就是說,這裏檢查:我們在put過程中,在鏈表中遍歷了幾個元素,如果遍歷了至少1個元素,這裏要進入下面的邏輯:檢查是否要擴容,因為,你binCount大於0,說明可能已經開始出現哈希衝突了。

  • 5處,取field:sizeCtl的值,給局部變量sc

  • 6處,判斷當前的新的鍵值對總數,是否大於sc了;比如容量是16,那麼sizeCtl是12,如果此時,hashmap中存放的鍵值對已經大於等於12了,則要檢查是否擴容了

  • 7處,幾個組合條件,查看是否要擴容,其中,主要的條件就是第6步的那個。

  • 8處,調用transfer,進行擴容

總結一下,經過前面的第6處,我們知道,如果存放的鍵值對總數,已經大於等於0.75*哈希桶(也就是底層數組的長度)的數量了,那麼,就基本要擴容了。

擴容的大體過程

擴容也是一個相對複雜的過程,這裏只說大概,詳細的放下講。

假設,現在底層數組長度,128,也就是128個哈希桶,當存放的鍵值對數量,大於等於 128 * 0.75的時候,就會開始擴容,擴容的過程,大概是:

  • 申請一個256(容量翻倍)的數組
  • 現在有128個桶,相當於,需要對128個桶進行遍歷,遍歷每個桶拉出去的鏈表或紅黑樹,查看每個鍵值對,是需要放到新數組的什麼位置

這個過程,昨天的博文,畫了個圖,這裏再貼一下。

擴容后:

可是,如果我們要一個個去遍歷所有哈希桶,然後遍歷對應的鏈表/紅黑樹,會不會太慢了?完全是單線程工作啊。

換個思路,我們能不能加快點呢?比如,線程1可以去處理數組的 0 -15這16個桶,16- 31這16個桶,完全可以讓線程2去做啊,這樣的話,不就多線程了嗎,不是就快了嗎?

沒錯,jdk就是這麼乾的。

jdk維護了一個field,這個field,專門用來存當前可以獲取的任務的索引,舉個例子:

大家看上圖就懂了,一開始,這裏假設我們有128個桶,每次每個線程,去拿16個桶來處理。

剛開始的時候,field:transferIndex就等於127,也就是最後一個桶的位置,然後我們要從后往前取,那麼,127 到112,剛好就是16個桶,所以,申請任務的時候,就會用cas去更新field為112,則表示,自己取到了112 到127這一個區間的hash桶遷移任務。

如果自始至終,只有一個線程呢,它處理完了112 – 127這一批hash桶后,會繼續取下一波任務,96 – 112;以此類推。

如果多線程的話呢,也是類似的,反正都是去嘗試cas更新transferIndex的值為任務區間的開始下標的值,成功了,就算任務認領成功了。

多線程,怎麼知道需要去幫助擴容呢? 發起擴容的線程,在處理完bucket[k]時,會把老的table中的對應的bucket[k]的頭節點,修改為下面這種類型的節點:

    static final class ForwardingNode<K,V> extends Node<K,V> {
        final Node<K,V>[] nextTable;
        ForwardingNode(Node<K,V>[] tab) {
            super(MOVED, null, null, null);
            this.nextTable = tab;
        }
    }

其他線程,在put或者其他操作時,發現頭結點變成了這個,就會去協助擴容了。

多線程擴容,和分段取任務的差別?

我個人感覺,差別不大,多線程擴容,就是多線程去獲取自己的那一段任務,然後來完成。我這邊寫了簡單的demo,不過感覺還是很有用的,可以幫助我們理解。

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.concurrent.*;
import java.util.concurrent.locks.LockSupport;

public class ConcurrentTaskFetch {

    /**
     * 空閑任務索引,獲取任務時,從該下標開始,往前獲取。
     * 比如當前下標為10,表示tasks數組中,0-10這個區間的任務,沒人領取
     */
    // 0
    private  volatile int freeTaskIndexForFetch;
	
    // 1
    private static final int TASK_COUNT_PER_FETCH = 16;
	
    // 2
    private String[] tasks = new String[128];

    public static void main(String[] args) {
        ConcurrentTaskFetch fetch = new ConcurrentTaskFetch();
        // 3
        fetch.init();

        ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100));
        executor.prestartAllCoreThreads();

        CyclicBarrier cyclicBarrier = new CyclicBarrier(10);
		
        // 4
        for (int i = 0; i < 10; i++) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        cyclicBarrier.await();
                    } catch (InterruptedException | BrokenBarrierException e) {
                        e.printStackTrace();
                    }
					
                    // 5
                    FetchedTaskInfo fetchedTaskInfo = fetch.fetchTask();
                    if (fetchedTaskInfo != null) {
                        System.out.println("thread:" + Thread.currentThread().getName() + ",get task success:" + fetchedTaskInfo);
                        try {
                            TimeUnit.SECONDS.sleep(3);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }

                        System.out.println("thread:" + Thread.currentThread().getName()  +  ", process task finished");
                    }
                }
            });
        }


        LockSupport.park();
    }

    public void init() {
        for (int i = 0; i < 128; i++) {
            tasks[i] = "task" + i;
        }
        freeTaskIndexForFetch = tasks.length;
    }

	// 6
    public FetchedTaskInfo fetchTask() {
        System.out.println("Thread start fetch task:"+Thread.currentThread().getName()+",time: "+System.currentTimeMillis());

        while (true){
			// 6.1
            if (freeTaskIndexForFetch == 0) {
                System.out.println("thread:" + Thread.currentThread().getName() + ",get task failed,there is no task");
                return null;
            }

            /**
             * 6.2 獲取當前任務的集合的上界
             */
            int subTaskListEndIndex = this.freeTaskIndexForFetch;

            /**
             * 6.3 獲取當前任務的集合的下界
             */
            int subTaskListStartIndex = subTaskListEndIndex > TASK_COUNT_PER_FETCH ?
                    subTaskListEndIndex - TASK_COUNT_PER_FETCH : 0;

            /**
             * 6.4
             * 現在,我們拿到了集合的上下界,即[subTaskListStartIndex,subTaskListEndIndex)
             * 該區間為前開后閉,所以,實際的區間為:
             * [subTaskListStartIndex,subTaskListEndIndex - 1]
             */

            /**
             * 6.5 使用cas,嘗試更新{@link freeTaskIndexForFetch} 為 subTaskListStartIndex
             */
            if (U.compareAndSwapInt(this, FREE_TASK_INDEX_FOR_FETCH, subTaskListEndIndex, subTaskListStartIndex)) {
                // 6.6 
                FetchedTaskInfo info = new FetchedTaskInfo();
                info.setStartIndex(subTaskListStartIndex);
                info.setEndIndex(subTaskListEndIndex - 1);


                return info;
            }
        }

    }



    // Unsafe mechanics
    private static final sun.misc.Unsafe U;

    private static final long FREE_TASK_INDEX_FOR_FETCH;

    static {
        try {
//            U = sun.misc.Unsafe.getUnsafe();
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            U = (Unsafe) f.get(null);
            Class<?> k = ConcurrentTaskFetch.class;
            FREE_TASK_INDEX_FOR_FETCH = U.objectFieldOffset
                    (k.getDeclaredField("freeTaskIndexForFetch"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }


    static class FetchedTaskInfo{
        int startIndex;
        int endIndex;

        public int getStartIndex() {
            return startIndex;
        }

        public void setStartIndex(int startIndex) {
            this.startIndex = startIndex;
        }

        public int getEndIndex() {
            return endIndex;
        }

        public void setEndIndex(int endIndex) {
            this.endIndex = endIndex;
        }

        @Override
        public String toString() {
            return "FetchedTaskInfo{" +
                    "startIndex=" + startIndex +
                    ", endIndex=" + endIndex +
                    '}';
        }
    }
}

  • 0處,定義了一個field,類似於前面的transferIndex

        /**
         * 空閑任務索引,獲取任務時,從該下標開始,往前獲取。
         * 比如當前下標為10,表示tasks數組中,0-10這個區間的任務,沒人領取
         */
        // 0
        private  volatile int freeTaskIndexForFetch;
    
  • 1,定義了每次取多少個任務,這裏也是16個

    private static final int TASK_COUNT_PER_FETCH = 16;
    
  • 2,定義任務列表,共128個任務

  • 3,main函數中,進行任務初始化

    public void init() {
        for (int i = 0; i < 128; i++) {
            tasks[i] = "task" + i;
        }
        freeTaskIndexForFetch = tasks.length;
    }
    

    主要初始化任務列表,其次,將freeTaskIndexForFetch 賦值為128,後續取任務,從這個下標開始

  • 4處,啟動10個線程,每個線程去執行取任務,按理說,我們128個任務,每個線程取16個,只能有8個線程取到任務,2個線程取不到

  • 5處,線程邏輯里,去獲取任務

  • 6處,獲取任務的方法定義

  • 6.1 ,如果可獲取的任務索引為0了,說明沒任務了,直接返回

  • 6.2,獲取當前任務的集合的上界

  • 6.3,獲取當前任務的集合的下界,減去16就行了

  • 6.4,拿到了集合的上下界,即[subTaskListStartIndex,subTaskListEndIndex)

  • 6.5, 使用cas,更新field為:6.4中的任務下界。

執行效果演示:

可以看到,8個線程取到任務,2個線程沒取到。

該思想在內存分配時的應用

其實jvm內存分配時,也是類似的思路,比如,設置堆內存為200m,那這200m是啟動時立馬從操作系統分配了的。

接下來,就是每次new對象的時候,去這個大內存里,找個小空間,這個過程,也是需要cas去競爭的,比如肯定也有個全局的字段,來表示當前可用內存的索引,比如該索引為100,表示,第100個字節后的空間是可以用的,那我要new個對象,這個對象有3個字段,需要大概30個字節,那我是不是需要把這個索引更新為130。

這中間是多線程的,所以也是要cas操作。

道理都是類似的。

總結

時間倉促,有問題在所難免,歡迎及時指出或加群討論。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

搭上末班車去了京東,終於可以做東哥兄弟…

介紹下自己

開篇先簡單介紹一下自己,雙非本科,大三在讀,通信學院物聯網工程專業。這個專業的發展方向大致分為軟、硬件兩種,大二的時候感覺自己更喜歡軟件方面,也就開始學習比較常用的 Java。

到了今年三月,開始投簡歷,投的都是 Java 開發工程師崗,參与春招實習生招聘,想體驗一下筆試、面試的過程,最好能拿到實習offer。

搭上了末班車

春招基本結束了,算是搭上了春招的末班車,被春招支配的恐懼也可以告一段落了,藉此機會梳理一下我的春招歷程。

被恐懼支配

剛開始心態很差,每過一面就會感覺很放鬆,可能會開心一下,這種開心的時候可能就不是很用心的在學習。

然而每掛一面就會非常沮喪,心情很低落,連續幾天都會整理不好心情繼續沉浸在學習中。還會懷疑自己是不是不行,是不是能力不夠,是不是….

心情起起落落,甚至在每面完一場,都會隔幾分鐘就刷新官網,看下流程情況,看下自己是不是掛掉了。焦灼的心,顫抖的手,浮躁的狀態。

完完全全被焦慮和恐懼給支配,沒心思學習,感覺自己整個思路、情緒、狀態都到了一個極點,而且是自己沒辦法突破的極點。

被恐懼支配比恐懼本身恐懼多了,而且後勁十足

我苦苦在尋找一個突破口,但一直沒能找到如何突破。如果找不到這個突破口,接下來的春招之路感覺很難走下去。

我遇到了龍叔

一次偶然在朋友圈看到同學轉發一篇文章,是說面試介紹的 當你面試“自我介紹”還在我是XXX時,看到這篇文章的同學們已經拿到了offer… ,看是關於面試的文章,就點進去看了。

看這篇文章確實補充了我很多盲點,於是我就點進去看了下其他的文章,比如這篇 學會龍叔這套面試秘訣,一套大招帶走面試官 ,發現裏面可以加交流群 和 加作者微信

還看到龍叔給粉絲輔導簡歷,於是我就鼓起勇氣加了龍叔微信,把自己的情況描述了下。抱着試試的態度,沒想到龍叔非常認真的回答了我,而且還在粉絲交流群里給大家說了。

非常幸運的遇到了龍叔,他告訴我,

面試完就不要總是等待結果,面試就像期末考,考完我們都不會再去翻書了。面完了,結果就不在我們考慮範圍之內了,要為下一場準備,也不能寄希望於一個公司,應該把心思放在複習和準備上。時刻保持自己的面試狀態,充滿鬥志、不要灰心。因為 offer 是需要流程的,不是面完就發,多準備,多面,之後就是收割 offer 的事情。天天憂心忡忡的,實在無濟於事,完全是浪費自己的時間。

我聽完犹如醍醐灌頂,受益匪淺,麻溜兒地寫在便簽紙上,貼在眼前,提醒自己。

人總有失意和遇到困難想不通的時候,而這時候能讓我的思想從短路變為通路,非常感謝龍叔。

成功的路總是不平坦的

之後四月份,身邊同學陸陸續續有收到 offer 的,去牛客網每次刷新的時候,也都是喜提校招或者實習 offer的記錄帖,羡慕、恰檸檬之餘。

我為自己還沒有理想的offer 感到發愁,又到了浮躁的一個新階段。這個階段雖說是浮躁,但是比起剛開始那一堵牆,已經好很多了。

會和朋友相互鼓勵,相互吐槽失敗的面試,心情 down 的時候聽聽大張偉的《陽光彩虹小白馬》 “你就是最強噠最棒噠最亮噠最發光噠”,努力讓自己平和、快樂,強行相信自己。很快就能調整好自己的狀態,繼續投入到戰鬥中。

心態太重要了,只有心態好了,複習才能更加有效率

這兩個月的面試中,讓我自己印象深刻的是騰訊三面,可能是傳說中的壓力面之類的。

當時操作系統學的不好,說明了之後,面試官還是在操作系統這方面窮追不舍地發問,從一開始的語氣溫柔、帶着笑意,到後來漸漸嚴肅、帶着凶意,與此同時我也意識到這最後一輪技術面多半是涼透了,要和我 say byebye了。

最後面試官甚至問 你到底有沒有學過操作系統?你是女生,為什麼要學開發?

當時的面試,我沒控制住情緒,為自己的菜流下了委屈的淚水

其實也沒什麼委屈的,畢竟菜是原罪,哈哈哈。可能因為人生第一回總監面,沒見過這種大場面的原因吧,還是要見多識廣啊。

事後反思,這樣實在是不合適,這是頂不住壓力的表現,面試官希望看到的,應該是沉着冷靜的,嘗試去解決問題的求職者,而不是這樣愛哭鼻子的。

一些總結

經歷這近三個月的面試,從開始自我介紹都結巴,到現在可以心跳正常地和面試官交流,收穫還是蠻多的。

我感到實力才是硬道理,結果的決定權在公司手裡,作為求職者,我們總是會被置於與其他同樣水平的人作比較的地位,只能不斷提高實力,才可能脫穎而出。

保持平和的心態會帶來一些好的運氣,還有就是堅持下去,最後一定會收穫好結果的。

從簡歷篩選、筆試、輪輪面試,一步一步過關,每次面試過程會錄音,之後復盤,通過復盤去看自己當時為什麼沒有回答上來,為什麼沒有收到面試官的青睞。

通過復盤,把不會的問題都搞明白,把該加分沒加上的,在後續的面試一定加上。面試完需要儘快查漏補缺,保持心態,堅持下去。

春季實習招聘還是比較寬容的,大廠也沒有因為我學歷不出色而不給面試機會,而且很多家的面試體驗還是很不錯的,有的面試官會引導我、會糾正我的錯誤、給予建議。

即使最後沒有通過,也是學到了一些東西,面試本身就是一種學習。

面試最好的狀態是和面試官交流,而不是硬生的回答。

最後希望秋招時,我可以擁有更平和的心態和更紮實的基礎,收穫自己心儀的offer~ ,也希望和我一起奮鬥的你們都能找到滿意的offer。

高頻考點

這裏列出遇到的面試中高頻的考點(被問到三次以上的那種~):

  • Java 基礎:HashMap源碼、泛型、NIO
  • 數據結構與算法:紅黑樹、堆、 海量數據中找top k 問題、 快速排序、堆排序
  • JVM :垃圾回收機制、Full GC、類加載機制
  • 數據庫:事務、索引、鎖、查詢優化、排查慢查詢
  • Spring 框架:IOC、AOP、事務、SpringMVC、常見註解
  • 操作系統:進程和線程、虛擬內存
  • 網絡:HTTPS、TCP三次握手四次揮手、HTTP狀態碼
  • 手撕算法:基本都是劍指offer上面的原題,還有 生產者-消費者模型
  • 再有,如果有讀過併發包中的源碼,或者對線程安全相關問題有自己的思考,也是很加分的。

這些是非常高頻的面試題,還有一些常規的,就不一一列舉了。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

※別再煩惱如何寫文案,掌握八大原則!

基於Azure IoT開發.NET物聯網應用系列-全新的Azure IoT架構

物聯網技術已經火了很多年了,業界各大廠商都有各自成熟的解決方案。我們公司主要搞新能源汽車充電,充電樁就是物聯網技術的最大應用,車聯網、物聯網、互聯網三網合一。2017年的時候重點研究過Azure IoT技術架構和使用,

Azure IoT 技術研究系列1-入門篇

隨着業界技術的發展,近期又重新關注並研究了最新的Azure  IoT架構,現在將結合著.NET Core技術和Azure IoT 做一些物聯網應用,將研究的成果分享給大家。

關於IoT的一些基本概念,重新梳理一下,分享給大家:

  • IoT:Internet of Things,即萬物互聯。
  • IoT Devices:物聯網設備。
  • IoT Edge Devices:物聯網邊緣計算設備。
  • IoT Gateway:IoT網關,負責IoT物聯網設備的接入、管理和控制、通訊(上行和下行)
  • 通訊協議:TCP、MQTT、AMQP、HTTPS、zgebee等等
  • Azure IoT Central。 IoT Central 是完全託管的 IoT物聯網 SaaS(軟件即服務)服務
  • 目前Azure仍然提供了Azure IoT Hub:直譯為Azure的物聯網中心
  • Azure IoT Hub為物聯網設備提供註冊、管理、溝通交互的雲服務。可用於管理數十億物聯網設備,提供可靠和安全的雲端與設備之間的雙向通信支持,每月可處理數以萬億計消息,並簡化了與其他Azure服務之間的集成,包括Azure機器學習以及
  • Azure流分析等。它是微軟Azure IoT Suite的重要組成部分,也是微軟物聯網戰略的重要基礎。

接下來,我們看一下Azure IoT最新的技術架構:

   

   下面,我們詳細介紹一下這個架構組成:

   一、 Things(物聯網設備側)

  1. IoT devices:前面已經介紹過了,泛指各類物聯網設備。設備可以安全地註冊到雲中,並且可以連接到雲之後,發送和接收數據。

  2. IoT edge devices:物聯網邊緣計算設備,某些設備可能會是在設備本身上或在現場網關中執行一些數據處理的邊緣設備。舉個大家平時常見的設備:充電樁,作為IoT邊緣計算設備,其自身有嵌入式操作系統、AI智能芯片,可以實現一些簡單的邊緣計算場景

  3. Cloud Gateway:雲網關,雲網關提供一個雲中心,以便設備安全地連接到雲併發送數據。 它還提供設備管理功能,包括設備的命令和控制。

      對於雲網關,Azure 建議使用Azure  IoT 中心。Azure IoT 中心是從設備引入事件的託管雲服務,充當設備與後端服務之間的消息代理。 同時提供安全連接、事件引入、雙向通信和設備管理。

      當然,我們也可以自建雲網關,支持各類物聯網設備的接入、管理和控制。

  4. Bulk devices provisioning:設備批量設置,統一管理設置海量設備。 對於註冊和連接許多組設備。可以使用 IoT 中心設備預配服務 (DPS)。 DPS 可用於大規模分配設備並將設備註冊到特定 Azure IoT 中心終結點。

二、Insights(洞察、洞見,可以理解為設備接入管理、數據處理、數據持久化、數據分析、可視化)

  1. Streaming Processing:流式數據處理

  Azure提供了專門的流分析服務。 流分析可以使用時間開窗函數、流聚合和外部數據源聯接大規模執行複雜分析。假如說我們自建系統做物聯網數據流式分析的話,可以使用Kafka、Flink、Spark等主流的大數據流式分析技術。

  2. Data transformation:數據轉換操作或聚合遙測數據流。

  常見的場景包括通訊協議轉換,例如,將二進制數據轉換為 JSON,或者合併數據點。 如果數據在到達 IoT 中心之前必須轉換,可以使用協議網關(一個可以轉換數據的網關)。 同時,數據可以在到達 IoT 中心後轉換。

  在這種情況下,可以使用 Azure Functions 函數計算,Azure Functions內置了與 IoT 中心、Cosmos DB 和 Blob 存儲的集成。

  3. Warm path store:熱存儲

  熱存儲,存儲實時物聯網設備上傳下發的數據,這些數據必須可按設備實時查詢,以用於報告和可視化。舉個實際的業務場景:充電樁實時上傳的電壓、電流、SOC等實時設備數據,這些數據的實時性要求高,可以存儲在熱存儲中。

  4. Cold path store:冷存儲

  如果所有的物聯網設備數據全部存儲在熱存儲中,其硬件成本會很高。數據具備一定的時效性,因為,當數據失去了一定的時效性要求后,可以存儲在冷存儲中,降低存儲的成本。

  這些數據會保留較長時間,用於批處理。 對於冷路徑存儲,可以使用 Azure Blob 存儲。 數據可無限期地以較低成本在 Blob 存儲中存檔,並且可以輕鬆訪問以進行批處理。

  5. UI Reporting and tools:可視化展現

  可視化展現方面,通常包含:IoT設備管理UI、設備控制UI、趨勢圖、連接狀態圖表、數據分析圖表等等,這個地方可以使用各類UI展現技術實現了。

三、 Action(運維管理、操作)

  1. Machine Learning:機器學習

   大家會問,用機器學習干什麼?通過歷史遙測數據執行模型訓練,實現IoT設備的預測性維護,同時還能做什麼?還可以對上報的數據建立不同的模型,實時進行訓練,智能控制設備。比如說充電樁的例子,動態調控充電功率,實現最大充電效率。

  2. Business integration:業務流程集成

   業務流程集成根據來自設備數據執行各類後續操作。 可以包括:存儲實時消息、引發警報、發送电子郵件或短信,或者與 CRM 集成。舉個實際的業務場景:當需要設備運維時,發出一個運維工單到產品運維部門,實現IoT設備的智能運維和派單處理。

  3. User Management:用戶管理

   用戶管理限制哪些用戶或組可以在設備上執行操作,例如升級固件。 它還定義應用程序中的用戶功能。

  綜上是Azure IoT架構的詳細介紹和說明,比2017年時,產品更加SaaS化,更加AI智能、更加體系。分享給大家。

 

 

周國慶

2020/6/7

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

上周熱點回顧(6.1-6.7)

熱點隨筆:

· Python驗證碼識別 (______null)
· .Net Core 會逆襲成為最受歡迎開發平台嗎? (葡萄城技術團隊)
· 從一年前的1200多人優化到現在200多人,待在這樣的技術團隊是一種什麼體驗? (沛山)
· 字符串太占內存了,我想了各種奇思淫巧對它進行壓縮 (一線碼農)
· 27歲了,程序員寫給自己的一封信 (學習Java的小姐姐)
· 和付費網盤說再見,跟着本文自己起個網盤(Java 開源項目) (削微寒)
· 啪啪,打臉了!領導說:try-catch必須放在循環體外! (Java中文社群)
· 同學叫我一起創業,我不聽,他現在月入10萬,我羡慕死了,我已悟到了成功的秘訣! (jonlan)
· ASP.NET Core Blazor Webassembly 之 數據綁定 (Agile.Zhou)
· MySql輕鬆入門系列——第一站 從源碼角度輕鬆認識mysql整體框架圖 (一線碼農)
· 我終於搞清了啥是 HTTPS 了 (極客挖掘機)
· 六一兒童節,程序員寫給女兒的一封信 (沉默王二)

熱點新聞:

· 月入兩萬的程序員背着電腦送外賣 好隨時改寫代碼
· 《紅警》重製版登上Steam暢銷榜:EA直接放出遊戲源代碼
· 擺攤吧,互聯網人!
· 一鍵“卸載中國應用”這款App,在印度火了
· 鄭皆連院士:中國是橋樑大國卻非橋樑強國 輸在了軟件上
· “刪除中國應用” App被下架,印度人表示氣憤,並喊話劈柴哥出面
· 你常吃的阿莫西林,正在引起一場災難
· 微軟新品被指剽竊!程序員開源兩年的成功項目被迫終結
· 獵鷹與龍飛船基於Linux採用 C++、Chromium與JS開發
· 泥坑裡爬出的任正非
· 唯美的李子柒,世俗的商業化
· 微信支持改 ID 之前,我在好友的微信號里發現了這些秘密

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

※別再煩惱如何寫文案,掌握八大原則!

過年前9萬預算買輛車!該買什麼車最實用?

其搭載的1。5L地球夢發動機,最大功率131馬力,峰值扭矩155牛米,和CVT變速箱搭配動力響應性出色,加速實力“有點猛”。很好地兼顧了動力以及油耗。空間實用的國貨SUV吉利汽車-遠景SUV指導價:7。49-10。19萬9萬元的預算也可以選擇現在火熱的國產SUV車型,它們空間實用,坐姿高、視野也不錯。

本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

※別再煩惱如何寫文案,掌握八大原則!

實至名歸 COC廈門站眾泰T600六連冠

運動版全身大幅採用超高張力鋼板,並在車身關鍵部位進行了強化,安全性進一步提升。在主動安全性方面,T600運動版更將安全防護展現得淋漓盡致,安全配一應俱全。ESC車身穩定系統、HAC上坡輔助系統、前後倒車雷達及360°可視倒車影像等安全配置,與6方位安全氣囊、盲點信息系統、紅外夜視系統、TpMS智能胎壓監測、可選裝的HUD抬頭显示系統等尖端科技配備聯合上演重重壁壘,出色安全,呼之欲出,滿滿自信應對挑戰,盡享出行便利。

12月6日,中國汽車場地越野錦標賽(COC)廈門站比賽圓滿結束,也是最後一場分站賽,在各組別激烈的決賽搶分大戰中,眾泰T600越野車隊在汽油廠商組中奪得頭籌,車手鹿丙龍奪取該組冠軍,並與隊友徐瑩一起為車隊捧回了廠商杯冠軍殊榮,從而擴大了在年度積分方面的領先優勢,眾泰已經在今年分站比賽中已獲得六連冠,高歌猛進,一步步接近年度總冠軍。

作為本年度分站賽的最後一站,各個車隊之間的競爭日趨白熱化,尤其是之前比賽積分接近的車隊及隊員,比賽前已經是“劍拔弩張”,力爭本站取得更好排名和積分。眾泰車隊隊長楊逵如是向記者說道:“相對來說,我們在汽油改裝組的優勢要大一些,汽油廠商組和奇瑞車隊比較接近,由於總決賽採用雙倍積分的賽制,能否最終獲得全年總冠軍,廈門站比賽顯得尤為重要”。

【場地航拍圖】

【車隊大營】

【眾泰T600戰車】

汽油廠商組

本次比賽最大的變化就是之前因嚴重違規被禁賽的長安CS75車隊,重新回到了比賽。針對COC廈門站比賽形勢的變化,眾泰T600越野車隊對參賽陣容也進行了微調,喬旭與刁志剛攜手出擊汽油改裝組,鹿丙龍回歸汽油廠商組,和徐瑩搭檔。

在6圈的第一輪預賽中,車手們都拿出渾身解數,以求跑出好成績,從而得到決賽中最好的發車位置。眾泰車隊的鹿丙龍和徐瑩不負眾望,以小組第三、第四的成績闖進決賽,一起進入決賽的還有長安CS75車隊的文凡和孟斌。

“我們自身和車輛都調整到了最好狀態,對下午進行的決賽充滿信心”,眾泰T600越野車隊的車手鹿丙龍在決賽前向記者如是說。決賽中,鹿丙龍的表現堪稱“完美”,以絕對優勢力壓長安CS75車隊的孟斌和文凡獲得本組冠軍,其隊友徐瑩獲得本組季軍,獲得本組亞軍的是來自長安CS75車隊的孟斌,同時,鹿丙龍和徐瑩為眾泰T600越野車隊爭得了汽油廠商組的車隊團體冠軍獎盃。

汽油改裝組

眾泰T600越野車隊的喬旭在第一輪預賽中並不順利,他在第三圈的時候賽車出現失誤,賽車在幾處急彎都發生失控打橫,這極大地影響了喬旭的成績,儘管第二輪成績出色,但仍與決賽失之交臂,其隊友刁志剛以小組第三的成績征戰第二天進行的決賽。

6日下午的決賽中,車手刁志剛一人獨自面對其他三位車手的多面夾擊,面對發車位置不力的劣勢,刁志剛仍然奮起直追,最後以微小差距獲得了本小組的季軍,獲得本組冠亞軍的是來自另外兩支車隊的趙向前和童振榮。

作為“主流價值SUV”的眾泰T600,同眾泰車隊一樣,已然成為乘用車銷售市場上的佼佼者,早已進入月銷量“萬台俱樂部”,2016年1-10月份更是實現了94371台的銷量,以月均近萬台的銷量位居自主品牌中型SUV銷量榜首。

而且2016年眾泰汽車推出了更為年輕時尚的眾泰T600運動版,作為在眾泰T600優勢平台上推出的車型,眾泰T600運動版同樣以其年輕時尚又不乏沉穩的外觀、越級的配置在整個市場中還是有着普遍好評,銷量也是芝麻開花節節高。

眾泰T600運動版全系標配10寸中控彩色大屏,內容豐富。而Tye-net智控系統的優勢融入,實現手機操遠程控愛車,娛樂隨行,舒心便利。

此外,眾泰T600運動版還配備了一鍵啟動/無鑰匙進入、紅外夜視系統、腳步感應式電動尾門等尖端科技配備,讓駕乘人員充分享受科技智能帶來的便捷體驗。電動全景天窗、电子駐車系統、前排座椅分級加熱、雙區獨立自動恆溫空調、手機無線充電、方向盤/座椅/后視鏡三項聯動記憶功能、全液晶儀錶盤、定速巡航等帶來更加細緻入微的貼心關懷,讓出行一路無虞。

安全配置方面,眾泰T600運動版同級領先的安全性讓駕乘者無需前瞻後顧,無憂外出。運動版全身大幅採用超高張力鋼板,並在車身關鍵部位進行了強化,安全性進一步提升。在主動安全性方面,T600運動版更將安全防護展現得淋漓盡致,安全配一應俱全。ESC車身穩定系統、HAC上坡輔助系統、前後倒車雷達及360°可視倒車影像等安全配置,與6方位安全氣囊、盲點信息系統、紅外夜視系統、TpMS智能胎壓監測、可選裝的HUD抬頭显示系統等尖端科技配備聯合上演重重壁壘,出色安全,呼之欲出,滿滿自信應對挑戰,盡享出行便利。

而眾泰T600運動版不只是在外觀上吸引目光,在內飾的色彩搭配上,更是可圈可點,整個車內空間看起來既神秘又科技時尚。

眾泰T600運動版擁有的2807mm的傲人軸距,有效保證了車輛的駕乘空間。車內豐富的儲物空間為日常儲物提供了便利,而且後排座椅放倒後進一步拓展了後備箱空間,可以盡情享受眾泰T600運動版帶來的寬適空間。

動力方面,T600運動版提供1.5T及2.0T兩種發動機車型,1.5T渦輪增壓發動機與5速手動變速器搭配出黃金動力組合,最大功率達119KW,最大扭矩達215N·m。更加值得期待的是其2.0T車型,搭配使用旋鈕換擋式6速雙離合或5速手動變速器,最大功率140KW,最大扭矩250N·m,百公里加速只需9.26秒,充分提高了燃油的利用率,更加的節能環保,同時降低了用車成本。眾泰T600運動版,就是這樣讓你既有“面子”,又有“裡子”。

還有值得一說的是,眾泰T600在2015年J.D.power亞太公司發布的中國新車質量研究(IQS)報告,眾泰T600在中型SUV中pp100(每百車問題數)為100,優於中型SUV平均水平(pp100:106),全國綜合排名第13位,位列中型SUV中國品牌第二名。

2016年度COC總決賽將於12月中旬在廣西柳州打響,總決賽將實行雙倍積分制,各組別總決賽冠軍將收穫50分,這也讓之前積分落後並不太多的車手擁有了翻身逆轉的機會,那眾泰T600能否攜勢而來,獲得全年比賽的總冠軍,讓我們拭目以待!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

ReentrantLock原理分析

一 UML類圖

1.1、ReentrantLock 

通過類圖ReentrantLock是同步鎖,同一時間只能有一個線程獲取到鎖,其他獲取該鎖的線程會被阻塞而被放入AQS阻塞隊列中。ReentrantLock類繼承Lock接口;內部抽象類Sync實現抽象隊列同步器AbstractQueuedSynchronizer;Sync類有兩個子類NonfairSync(非公平鎖)和FairSync(公平鎖),默認構造方法使用非公平鎖,可以使用帶布爾參數的構造方法指定使用公平鎖;ReentrantLock可以創建多個條件進行綁定。

1.2、AbstractQueuedSynchronizer

AbstractQueuedSynchronizer:抽象隊列同步器,維護一個volatile int state變量標識共享資源和一個FIFO線程阻塞隊列(AQS隊列)。

protected final void setState(int newState):設置state值

protected final int getState():獲取state值

protected final boolean compareAndSetState(int expect, int update):CAS設置state值

AQS有兩種共享資源類型:SHARED(共享)和EXCLUSIVE(獨佔),針對類型有不同的方法:

protected boolean tryAcquire(int arg):獨佔類型獲取鎖

protected boolean tryRelease(int arg):獨佔類型釋放鎖

protected int tryAcquireShared(int arg):共享類型獲取鎖

protected boolean tryReleaseShared(int arg):共享類型釋放鎖

protected boolean isHeldExclusively():是否是獨佔類型

1.3、線程節點類型waitStatus

AQS隊列中節點的waitStatus枚舉值(java.util.concurrent.locks.AbstractQueuedSynchronizer.Node)含義:

 static final int CANCELLED = 1; //線程被取消

static final int SIGNAL = -1; //成功的線程需要被喚醒
static final int CONDITION = -2; //線程在條件隊列中等待
static final int PROPAGATE = -3; //釋放鎖是需要通知其他節點

二 原理分析

2.1 獲取鎖

2.1.1 void lock()方法

調用線程T調用該方法嘗試獲取當前鎖。

①如果鎖未被其他線程獲取,則調用線程T成功獲取到當前鎖,然後設置當前鎖的擁有者為調用線程T,並設置AQS的狀態值state為1,然後直接返回。

②如果調用線程T之前已經獲取當前鎖,則只設置設置AQS的狀態值state加1,然後返回。

③如果當前鎖已被其他線程獲取,則調用線程T放入AQS隊列后阻塞掛起。

public void lock() {
    sync.lock();//委託內部公平鎖和非公平鎖獲取鎖
} 
//非公平鎖
final
void lock() { if (compareAndSetState(0, 1))//設置AQS狀態值為1 setExclusiveOwnerThread(Thread.currentThread());//成功設置鎖的線程擁有者 else acquire(1);//失敗加入到AQS隊列阻塞掛起 } //公平鎖 final void lock() { acquire(1); }

非公平鎖分析:

//調用父類AbstractOwnableSynchronizer方法CAS設置state值,成功返回true,失敗返回false
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
//調用父類AbstractOwnableSynchronizer方法,設置當前線程為鎖的擁有者
protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}
//調用AbstractQueuedSynchronizer父類方法,第一次獲取鎖失敗
public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//排它鎖類型
        selfInterrupt();
}
//NonfairSync子類,重寫父類方法
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
//Sync類非公平鎖嘗試獲取鎖
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {//二次獲取鎖 if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {//當前線程已獲取鎖,AQS狀態值加1 int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
//AbstractQueuedSynchronizer類創建節點,添加到AQS隊列後面
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);//創建AQS隊列的節點,節點類型排它鎖
    Node pred = tail;//尾結點 if (pred != null) {
        node.prev = pred;//新節點的前一個節點是尾結點 if (compareAndSetTail(pred, node)) {//CAS機制添加節點
            pred.next = node;//尾結點執行新的節點 return node;
        }
    }
    enq(node);
    return node;
}
//插入節點到隊列中
private
Node enq(final Node node) { for (;;) {//循環的方式,直至創建成功 Node t = tail;//尾結點 if (t == null) { //尾結點為空,初始化 if (compareAndSetHead(new Node()))//第一步:CAS創建頭結點(哨兵節點)一個空節點 tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) {//第二步:CAS設置尾結點 t.next = node; return t; } } } }
//
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();//前向節點 if (p == head && tryAcquire(arg)) {//如果p節點是頭結點,node作為隊列第二個節點
                setHead(node);//將頭節點設置為node節點,node節點出隊列
                p.next = null; //原頭結點設置為空,便於GC回收
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);//失敗解鎖
    }
}
private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}
//阻塞掛起當前線程
static void selfInterrupt() {
    Thread.currentThread().interrupt();
}
//
private
static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) /* * This node has already set status asking a release * to signal it, so it can safely park. */ return true; if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0);//大於0代表線程被取消 pred.next = node; } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } //線程阻塞,打斷線程 private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }

公平鎖分析:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {//與非公平鎖相比,區別就在標紅的位置
            setExclusiveOwnerThread(current);
            return true;
        }
    }else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
  //①h != t:表示AQS隊列頭結點和尾結點不相同,隊列不為空;
  //②(s = h.next) == null || s.thread != Thread.currentThread():頭結點(哨兵節點)為空或者next節點不等於當前線程
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }

 2.1.2 void lockInterruptibly()方法

與2.2.1方法相似,不同之處在於:該方法對中斷進行響應,其他線程調用當前線程中斷方法,拋出InterruptedException。

2.1.3 boolean tryLock()方法

嘗試獲取鎖。注意:該方法不會引起當前線程阻塞。

2.1.4 boolean tryLock(long timeout, TimeUnit unit)方法

與2.1.3方法相似,不同之處在於:設置了超時時間。

2.2 釋放鎖

嘗試釋放鎖。

如果當前線程T已獲取鎖,則調用該方法更新AQS狀態值減1。如果當前狀態值為0,則釋放鎖;如果當前狀態值部位0,則只是減1操作。

如果當前線程T未獲取鎖,則調用該方法是會拋出IllegalMonitorStateException異常。

2.2.1 void unlock()方法

public void unlock() {
    sync.release(1);
}
//調用AbstractQueuedSynchronizer方法
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);//喚醒線程 return true;
    }
    return false;
}
//回調Sync類釋放鎖
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);//設置鎖的擁有線程為空
    }
    setState(c);
    return free;
}
//
private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;//線程阻塞等待狀態 if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);//CAS設置狀態 /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)//遍歷AQS隊列 if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);//喚醒線程
}

 

h != t

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

※別再煩惱如何寫文案,掌握八大原則!

.NET Web應用中為什麼要使用async/await異步編程

前言

  1. 什麼是async/await?
    await和async是.NET Framework4.5框架、C#5.0語法裏面出現的技術,目的是用於簡化異步編程模型。

  2. async和await的關係?
    async和await是成對出現的。
    async出現在方法的聲明裡,用於批註一個異步方法。光有async是沒有意義的。
    await出現在方法內部,Task前面。只能在使用async關鍵字批註的方法中使用await關鍵字。

        private async Task DoSomething()
        {
            await Task.Delay(TimeSpan.FromSeconds(10));
        }
  1. async/await會創建新的線程嗎?
    不會。async/await關鍵字本身是不會創建新的線程的,但是被await的方法內部一般會創建新的線程。

  2. asp.net mvc/webapi action中使用async/await會提高請求的響應速度嗎?
    不會。

正題

我們都知道web應用不同於winform、wpf等客戶端應用,客戶端應用為了保證UI渲染的一致性往往都是採用單線程模式,這個UI線程稱為主線程,如果在主線程做耗時操作就會導致程序界面假死,所以客戶端開發中使用多線程異步編程非常必要。
可web應用本身就是多線程模式,服務器會為每個請求分配工作線程。
既然async/await不能創建新線程,又不能使提高請求的響應速度,那.NET Web應用中為什麼要使用async/await異步編程呢?

在 web 服務器上,.NET Framework 維護用於處理 ASP.NET 請求的線程池。 當請求到達時,將調度池中的線程以處理該請求。 如果以同步方式處理請求,則處理請求的線程將在處理請求時處於繁忙狀態,並且該線程無法處理其他請求。

在啟動時看到大量併發請求的 web 應用中,或具有突發負載(其中併發增長突然增加)時,使 web 服務調用異步會提高應用程序的響應能力。 異步請求與同步請求所需的處理時間相同。 如果請求發出需要兩秒鐘時間才能完成的 web 服務調用,則該請求將需要兩秒鐘,無論是同步執行還是異步執行。 但是,在異步調用期間,線程在等待第一個請求完成時不會被阻止響應其他請求。 因此,當有多個併發請求調用長時間運行的操作時,異步請求會阻止請求隊列和線程池的增長。

下面用代碼來實際測試一下:

  • 先是同步的方式,代碼很簡單,就是輸出一下請求開始和結束的時間和線程ID:
        public ActionResult Index()
        {
            DateTime startTime = DateTime.Now;//進入DoSomething方法前的時間
            var startThreadId = Thread.CurrentThread.ManagedThreadId;//進入DoSomething方法前的線程ID

            DoSomething();//耗時操作

            DateTime endTime = DateTime.Now;//完成DoSomething方法的時間
            var endThreadId = Thread.CurrentThread.ManagedThreadId;//完成DoSomething方法后的線程ID
            return Content($"startTime:{ startTime.ToString("yyyy-MM-dd HH:mm:ss:fff") } startThreadId:{ startThreadId }<br/>endTime:{ endTime.ToString("yyyy-MM-dd HH:mm:ss:fff") } endThreadId:{ endThreadId }<br/><br/>");
        }

        /// <summary>
        /// 耗時操作
        /// </summary>
        /// <returns></returns>
        private void DoSomething()
        {
            Thread.Sleep(10000);
        }

使用瀏覽器開3個標籤頁進行測試(因為瀏覽器對同一域名下的連接數有限制,一般是6個左右,所以就弄3個吧):

可以看到耗時都是10秒,開始和結束的線程ID一致。

  • 下面改造成異步的:
        public async Task<ActionResult> Index()
        {
            DateTime startTime = DateTime.Now;//進入DoSomething方法前的時間
            var startThreadId = Thread.CurrentThread.ManagedThreadId;//進入DoSomething方法前的線程ID

            await DoSomething();//耗時操作

            DateTime endTime = DateTime.Now;//完成DoSomething方法的時間
            var endThreadId = Thread.CurrentThread.ManagedThreadId;//完成DoSomething方法后的線程ID
            return Content($"startTime:{ startTime.ToString("yyyy-MM-dd HH:mm:ss:fff") } startThreadId:{ startThreadId }<br/>endTime:{ endTime.ToString("yyyy-MM-dd HH:mm:ss:fff") } endThreadId:{ endThreadId }<br/><br/>");
        }

        /// <summary>
        /// 耗時操作
        /// </summary>
        /// <returns></returns>
        private async Task DoSomething()
        {
            await Task.Run(() => Thread.Sleep(10000));
        }

結果:

可以看到3次請求中,雖然耗時都是10秒,但是出現了開始和結束的線程ID不一致的情況,ID為22的這個線程工作了多次,這意味着使用異步方式在同一時間可以處理更多的請求!(這句話不太對,3個同步的併發請求必然會分配3個工作線程,但是使用異步的話,同一個線程可以被多個請求重複利用。因為線程池的線程數量是有上限的,所以在相同數量的線程下,使用異步方式能處理更多的請求。)
IIS默認隊列長度:

await關鍵字不會阻塞線程直到任務完成。 它將方法的其餘部分註冊為任務的回調,並立即返回。 當await的任務最終完成時,它將調用該回調,並因此在其中斷時繼續執行方法。

簡單來說:就是使用同步方法時,線程會被耗時操作一直佔有,直到耗時操作完成。而使用異步方法,程序走到await關鍵字時會立即return,釋放線程,餘下的代碼會放進一個回調中(Task.GetAwaiter()的UnsafeOnCompleted(Action)回調),耗時操作完成時才會回調執行,所以async/await是語法糖,其本質是一個狀態機。

那是不是所有的action都要用async/await呢?
不是。一般的磁盤IO或者網絡請求等耗時操作才考慮使用異步,不要為了異步而異步,異步也是需要消耗性能的,使用不合理會適得其反。

結論

async/await異步編程不能提升響應速度,但是可以提升響應能力(吞吐量)。異步和同步各有優劣,要合理選擇,不要為了異步而異步。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

保存良好!擁2.3米長牙巨象遺骸出土 曾被分屍證據曝光

摘錄自2020年5月21日自由時報報導

德國圖賓根大學(University of Tuebingen)發表新研究報告,稱在該國西北部下薩克森邦一處遺址發現至少10頭巨象的遺骸,其狀態從舊石器時代至今仍保持良好,且按現場痕跡判斷,這些巨象死後曾被獵人分屍取肉,目前無法確定是自然死亡或遭人獵殺。

綜合外媒報導,德國圖賓根大學考古團隊針對下薩克森邦舍寧根遺址(Schoningen)出土的歐洲菱齒象遺骸進行復原與研究,團隊指出,其中一頭體型巨大、保存良好的雌象擁有長達2.3公尺的巨牙,研究人員認為,牠在距今約30萬年前死亡,死後遺骸曾被獵人分割。

負責挖掘行動的考古學家塞蘭格利(Jordi Serangeli)指出,在復原這頭雌象遺骸後推斷其肩高約3.2公尺,重約6.8噸,體型大於現今仍存在於世的非洲象。

生活環境
國際新聞
德國
古菱齒象
化石

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!