死磕 java集合之ConcurrentHashMap源碼分析(三)

来源:https://www.cnblogs.com/tong-yuan/archive/2019/04/10/ConcurrentHashMap-sourcecode.html
-Advertisement-
Play Games

死磕 java集合之ConcurrentHashMap源碼分析(三) ConcurrentHashMap查詢是否也加鎖? ConcurrentHashMap有哪些值得我們學習的技術? ConcurrentHashMap有哪些不能解決的問題? ...


本章接著上兩章,鏈接直達:

死磕 java集合之ConcurrentHashMap源碼分析(一)

死磕 java集合之ConcurrentHashMap源碼分析(二)


刪除元素

刪除元素跟添加元素一樣,都是先找到元素所在的桶,然後採用分段鎖的思想鎖住整個桶,再進行操作。

public V remove(Object key) {
    // 調用替換節點方法
    return replaceNode(key, null, null);
}

final V replaceNode(Object key, V value, Object cv) {
    // 計算hash
    int hash = spread(key.hashCode());
    // 自旋
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0 ||
                (f = tabAt(tab, i = (n - 1) & hash)) == null)
            // 如果目標key所在的桶不存在,跳出迴圈返回null
            break;
        else if ((fh = f.hash) == MOVED)
            // 如果正在擴容中,協助擴容
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            // 標記是否處理過
            boolean validated = false;
            synchronized (f) {
                // 再次驗證當前桶第一個元素是否被修改過
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        // fh>=0表示是鏈表節點
                        validated = true;
                        // 遍歷鏈表尋找目標節點
                        for (Node<K,V> e = f, pred = null;;) {
                            K ek;
                            if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                            (ek != null && key.equals(ek)))) {
                                // 找到了目標節點
                                V ev = e.val;
                                // 檢查目標節點舊value是否等於cv
                                if (cv == null || cv == ev ||
                                        (ev != null && cv.equals(ev))) {
                                    oldVal = ev;
                                    if (value != null)
                                        // 如果value不為空則替換舊值
                                        e.val = value;
                                    else if (pred != null)
                                        // 如果前置節點不為空
                                        // 刪除當前節點
                                        pred.next = e.next;
                                    else
                                        // 如果前置節點為空
                                        // 說明是桶中第一個元素,刪除之
                                        setTabAt(tab, i, e.next);
                                }
                                break;
                            }
                            pred = e;
                            // 遍歷到鏈表尾部還沒找到元素,跳出迴圈
                            if ((e = e.next) == null)
                                break;
                        }
                    }
                    else if (f instanceof TreeBin) {
                        // 如果是樹節點
                        validated = true;
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> r, p;
                        // 遍歷樹找到了目標節點
                        if ((r = t.root) != null &&
                                (p = r.findTreeNode(hash, key, null)) != null) {
                            V pv = p.val;
                            // 檢查目標節點舊value是否等於cv
                            if (cv == null || cv == pv ||
                                    (pv != null && cv.equals(pv))) {
                                oldVal = pv;
                                if (value != null)
                                    // 如果value不為空則替換舊值
                                    p.val = value;
                                else if (t.removeTreeNode(p))
                                    // 如果value為空則刪除元素
                                    // 如果刪除後樹的元素個數較少則退化成鏈表
                                    // t.removeTreeNode(p)這個方法返回true表示刪除節點後樹的元素個數較少
                                    setTabAt(tab, i, untreeify(t.first));
                            }
                        }
                    }
                }
            }
            // 如果處理過,不管有沒有找到元素都返回
            if (validated) {
                // 如果找到了元素,返回其舊值
                if (oldVal != null) {
                    // 如果要替換的值為空,元素個數減1
                    if (value == null)
                        addCount(-1L, -1);
                    return oldVal;
                }
                break;
            }
        }
    }
    // 沒找到元素返回空
    return null;
}

(1)計算hash;

(2)如果所在的桶不存在,表示沒有找到目標元素,返回;

(3)如果正在擴容,則協助擴容完成後再進行刪除操作;

(4)如果是以鏈表形式存儲的,則遍歷整個鏈表查找元素,找到之後再刪除;

(5)如果是以樹形式存儲的,則遍歷樹查找元素,找到之後再刪除;

(6)如果是以樹形式存儲的,刪除元素之後樹較小,則退化成鏈表;

(7)如果確實刪除了元素,則整個map元素個數減1,並返回舊值;

(8)如果沒有刪除元素,則返回null;

獲取元素

獲取元素,根據目標key所在桶的第一個元素的不同採用不同的方式獲取元素,關鍵點在於find()方法的重寫。

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    // 計算hash
    int h = spread(key.hashCode());
    // 如果元素所在的桶存在且裡面有元素
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
        // 如果第一個元素就是要找的元素,直接返回
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        else if (eh < 0)
            // hash小於0,說明是樹或者正在擴容
            // 使用find尋找元素,find的尋找方式依據Node的不同子類有不同的實現方式
            return (p = e.find(h, key)) != null ? p.val : null;

        // 遍歷整個鏈表尋找元素
        while ((e = e.next) != null) {
            if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

(1)hash到元素所在的桶;

(2)如果桶中第一個元素就是該找的元素,直接返回;

(3)如果是樹或者正在遷移元素,則調用各自Node子類的find()方法尋找元素;

(4)如果是鏈表,遍歷整個鏈表尋找元素;

(5)獲取元素沒有加鎖;

獲取元素個數

元素個數的存儲也是採用分段的思想,獲取元素個數時需要把所有段加起來。

public int size() {
    // 調用sumCount()計算元素個數
    long n = sumCount();
    return ((n < 0L) ? 0 :
            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
                    (int)n);
}

final long sumCount() {
    // 計算CounterCell所有段及baseCount的數量之和
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

(1)元素的個數依據不同的線程存在在不同的段里;(見addCounter()分析)

(2)計算CounterCell所有段及baseCount的數量之和;

(3)獲取元素個數沒有加鎖;

總結

(1)ConcurrentHashMap是HashMap的線程安全版本;

(2)ConcurrentHashMap採用(數組 + 鏈表 + 紅黑樹)的結構存儲元素;

(3)ConcurrentHashMap相比於同樣線程安全的HashTable,效率要高很多;

(4)ConcurrentHashMap採用的鎖有 synchronized,CAS,自旋鎖,分段鎖,volatile等;

(5)ConcurrentHashMap中沒有threshold和loadFactor這兩個欄位,而是採用sizeCtl來控制;

(6)sizeCtl = -1,表示正在進行初始化;

(7)sizeCtl = 0,預設值,表示後續在真正初始化的時候使用預設容量;

(8)sizeCtl > 0,在初始化之前存儲的是傳入的容量,在初始化或擴容後存儲的是下一次的擴容門檻;

(9)sizeCtl = (resizeStamp << 16) + (1 + nThreads),表示正在進行擴容,高位存儲擴容郵戳,低位存儲擴容線程數加1;

(10)更新操作時如果正在進行擴容,當前線程協助擴容;

(11)更新操作會採用synchronized鎖住當前桶的第一個元素,這是分段鎖的思想;

(12)整個擴容過程都是通過CAS控制sizeCtl這個欄位來進行的,這很關鍵;

(13)遷移完元素的桶會放置一個ForwardingNode節點,以標識該桶遷移完畢;

(14)元素個數的存儲也是採用的分段思想,類似於LongAdder的實現;

(15)元素個數的更新會把不同的線程hash到不同的段上,減少資源爭用;

(16)元素個數的更新如果還是出現多個線程同時更新一個段,則會擴容段(CounterCell);

(17)獲取元素個數是把所有的段(包括baseCount和CounterCell)相加起來得到的;

(18)查詢操作是不會加鎖的,所以ConcurrentHashMap不是強一致性的;

(19)ConcurrentHashMap中不能存儲key或value為null的元素;

彩蛋——值得學習的技術

ConcurrentHashMap中有哪些值得學習的技術呢?

我認為有以下幾點:

(1)CAS + 自旋,樂觀鎖的思想,減少線程上下文切換的時間;

(2)分段鎖的思想,減少同一把鎖爭用帶來的低效問題;

(3)CounterCell,分段存儲元素個數,減少多線程同時更新一個欄位帶來的低效;

(4)@sun.misc.Contended(CounterCell上的註解),避免偽共用;(p.s.偽共用我們後面也會講的^^)

(5)多線程協同進行擴容;

(6)你又學到了哪些呢?

彩蛋——不能解決的問題

ConcurrentHashMap不能解決什麼問題呢?

請看下麵的例子:

private static final Map<Integer, Integer> map = new ConcurrentHashMap<>();

public void unsafeUpdate(Integer key, Integer value) {
    Integer oldValue = map.get(key);
    if (oldValue == null) {
        map.put(key, value);
    }
}

這裡如果有多個線程同時調用unsafeUpdate()這個方法,ConcurrentHashMap還能保證線程安全嗎?

答案是不能。因為get()之後if之前可能有其它線程已經put()了這個元素,這時候再put()就把那個線程put()的元素覆蓋了。

那怎麼修改呢?

答案也很簡單,使用putIfAbsent()方法,它會保證元素不存在時才插入元素,如下:

public void safeUpdate(Integer key, Integer value) {
    map.putIfAbsent(key, value);
}

那麼,如果上面oldValue不是跟null比較,而是跟一個特定的值比如1進行比較怎麼辦?也就是下麵這樣:

public void unsafeUpdate(Integer key, Integer value) {
    Integer oldValue = map.get(key);
    if (oldValue == 1) {
        map.put(key, value);
    }
}

這樣的話就沒辦法使用putIfAbsent()方法了。

其實,ConcurrentHashMap還提供了另一個方法叫replace(K key, V oldValue, V newValue)可以解決這個問題。

replace(K key, V oldValue, V newValue)這個方法可不能亂用,如果傳入的newValue是null,則會刪除元素。

public void safeUpdate(Integer key, Integer value) {
    map.replace(key, 1, value);
}

那麼,如果if之後不是簡單的put()操作,而是還有其它業務操作,之後才是put(),比如下麵這樣,這該怎麼辦呢?

public void unsafeUpdate(Integer key, Integer value) {
    Integer oldValue = map.get(key);
    if (oldValue == 1) {
        System.out.println(System.currentTimeMillis());
        /**
         * 其它業務操作
         */
        System.out.println(System.currentTimeMillis());
      
        map.put(key, value);
    }
}

這時候就沒辦法使用ConcurrentHashMap提供的方法了,只能業務自己來保證線程安全了,比如下麵這樣:

public void safeUpdate(Integer key, Integer value) {
    synchronized (map) {
        Integer oldValue = map.get(key);
        if (oldValue == null) {
            System.out.println(System.currentTimeMillis());
            /**
             * 其它業務操作
             */
            System.out.println(System.currentTimeMillis());

            map.put(key, value);
        }
    }
}

這樣雖然不太友好,但是最起碼能保證業務邏輯是正確的。

當然,這裡使用ConcurrentHashMap的意義也就不大了,可以換成普通的HashMap了。

上面只是舉一個簡單的例子,我們不能聽說ConcurrentHashMap是線程安全的,就認為它無論什麼情況下都是線程安全的,還是那句話盡信書不如無書。

這也正是我們讀源碼的目的之一,瞭解其本質,才能在我們的實際工作中少挖坑,不論是挖給別人還是挖給自己^^。


qrcode


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

-Advertisement-
Play Games
更多相關文章
  • .net4.5部署到docker容器 1. 部署到windows容器 2. 部署到linux容器 部署到windows容器 由於.net本身就是運行在windows平臺的,所以它與windows容器也是更加適合,你可以以iis鏡像為基礎,去編寫你的Dockerfile文件,從而去構建你的.net項目 ...
  • 前言 ASP.NET Core 中 繼承的是AuthorizationHandler ,而ASP.NET Framework 中繼承的是AuthorizeAttribute. 它們都是用過重寫裡面的方法實現過濾請求的。 現在我們實現如何在 ASP.NET Core MVC 實現自定義授權。 關於Au ...
  • 一、什麼是外觀模式 定義:為子系統中的一組介面提供一個一致的界面,用來訪問子系統中的一群介面。 外觀模式組成: Facade:負責子系統的的封裝調用 Subsystem Classes:具體的子系統,實現由外觀模式Facade對象來調用的具體任務 二、外觀模式的使用場景 1、設計初期階段,應該有意識 ...
  • 前言: 相信很多人都聽過一個問題:把大象關進冰箱門,需要幾步? 第一,把冰箱門打開;第二,把大象放進去;第三,把冰箱門關上。我們可以看見,這個問題的答案回答的很有步驟。接下來我們介紹一種設計模式——模板方法模式,你會發現,它與這個問題的答案實際上有很多共同之處。 一、定義 定義一個演算法骨架,允許子類 ...
  • 簡介 CAT 是一個實時和接近全量的監控系統,它側重於對Java應用的監控,基本接入了美團上海所有核心應用。目前在中間件(MVC、RPC、資料庫、緩存等)框架中得到廣泛應用,為美團各業務線提供系統的性能指標、健康狀況、監控告警等。 優勢 實時處理:信息的價值會隨時間銳減,尤其是事故處理過程中。 全量 ...
  • 想著想著結果還是過了時間! 這個月的進度堪比之前幾年! 我自己的游戲用UE4寫的風聲水起! 美術的朋友給我做了好多動作! 我都想著以後弄個個人眾籌什麼的了,可見是進展不錯! 公司的事最近不太忙,反而是結婚的事越來越忙,戒指已經買完了,50分的也算不小了,1克拉的她也嫌貴! 前倆禮拜還順便把MACOS ...
  • 好玩圖像PIL處理 一、PIL庫學習總結 1、PIL中的模塊 Image模塊、ImageChops模塊、ImageCrackCode模塊、ImageDraw模塊、ImageEnhance模塊、ImageFile模塊、ImageFileIO模塊、ImageFilter模塊、ImageFont模塊、Im ...
  • # 從決定學習編程語言到正式做出計劃擠出空餘時間,歷經一年半,因工作原因及生活原因不斷擱淺,從湖北到浙江再回湖北,暫時穩定在一家小公司,從日常加班中壓縮時間學習,於此記錄學習進度、學習問題,在此過程中望前輩們不吝指教,自己也會堅持住,希望能早日做到技術實現,在此領域不斷成長! 至此,人生苦短,我用p ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...