搜尋
首頁Javajava教程java無鎖hashmap原理與實作詳解

java多線程環境中應用HashMap,主要有以下幾種選擇:使用線程安全的java.util.Hashtable作為替代使用java.util.Collections.synchronizedMap方法,將現有的HashMap物件包裝為線程安全的。使用java.util.concurrent.ConcurrentHashMap類別作為替代,它具有非常好的效能。
而以上幾種方法在實現的具體細節上,都或多或少地用到了互斥鎖。互斥鎖會造成執行緒阻塞,降低運作效率,並有可能產生死鎖、優先權翻轉等一系列問題。

CAS(Compare And Swap)是一種底層硬體提供的功能,它可以將判斷並更改一個值的操作原子化。

Java中的原子操作

在java.util.concurrent.atomic套件中,Java為我們提供了許多方便的原子類型,它們底層完全基於CAS操作。

例如我們希望實作一個全域公用的計數器,那麼可以:

privateAtomicInteger counter =newAtomicInteger(3); 

publicvoidaddCounter() { 

    for(;;) { 

        intoldValue = counter.get(); 

        intnewValue = oldValue +1; 

        if(counter.compareAndSet(oldValue, newValue)) 

            return; 

    } 

}

其中,compareAndSet方法會檢查counter現有的值是否為oldValue,如果是,則將其設為新值newValue,操作成功並傳回true ;否則操作失敗並回傳false。

當計算counter新值時,若其他執行緒將counter的值改變,compareAndSwap就會失敗。此時我們只需在外面增加一層循環,不斷嘗試這個過程,那麼最終一定會成功將counter值+1。 (其實AtomicInteger已經為常用的+1/-1操作定義了incrementAndGet與decrementAndGet方法,以後我們只需簡單調用它即可)

除了AtomicInteger外,java.util.concurrent.atomic包還提供了AtomicReference和AtomicReferenceArrayray類型,它們分別代表原子性的引用和原子性的引用數組(引用的數組)。

無鎖鍊錶的實作
在實作無鎖HashMap之前,讓我們先來看看比較簡單的無鎖鍊錶的實作方法。

以插入操作為例:

首先我們需要找到待插入位置前面的節點A和後面的節點B。
接著新建一個節點C,並使其next指標指向節點B。 (見圖1)
最後使節點A的next指標指向節點C。 (見圖2)

java無鎖hashmap原理與實作詳解

但在操作中途,有可能其他執行緒在A與B直接也插入了一些節點(假設為D),如果我們不做任何判斷,可能造成其他執行緒插入節點的遺失。 (見圖3)我們可以利用CAS操作,在為節點A的next指標賦值時,判斷其是否仍指向B,如果節點A的next指標發生了變化則重試整個插入操作。大致程式碼如下:

privatevoidlistInsert(Node head, Node c) { 

 
    for(;;) { 

 
        Node a = findInsertionPlace(head), b = a.next.get(); 

 
        c.next.set(b); 

        if(a.next.compareAndSwap(b,c)) 

            return; 
    } 
}

(Node類別的next欄位為AtomicReference型別,即指向Node型別的原子性參考)

無鎖鍊錶的查找操作與普通鍊錶沒有差別。而其刪除操作,則需要找到待刪除節點前方的節點A和後方的節點B,利用CAS操作驗證並更新節點A的next指針,使其指向節點B。

無鎖HashMap的困難與突破
HashMap主要有插入、刪除、查找、ReHash四種基本操作。一個典型的HashMap實現,會用到一個數組,數組的每個元素為一個節點的鍊錶。對於此鍊錶,我們可以利用上文提到的操作方法,執行插入、刪除以及查找操作,但對於ReHash操作則比較困難。

java無鎖hashmap原理與實作詳解

如圖4,在ReHash過程中,一個典型的操作是遍歷舊表中的每個節點,計算其在新表中的位置,然後將其移動至新表中。期間我們需要操縱3次指針:

將A的next指針指向D
將B的next指針指向C
將C的next指針指向E
而這三次指針操作必須同時完成,才能確保移動操作的原子性。但我們不難看出,CAS操作每次只能保證一個變數的值被原子性地驗證並更新,無法滿足同時驗證並更新三個指標的需求。

於是我們不妨換一個思路,既然移動節點的操作如此困難,我們可以使所有節點始終保持有序狀態,從而避免了移動操作。在典型的HashMap實作中,數組的長度始終保持為2i,而從Hash值映射為數組下標的過程,只是簡單地對數組長度執行取模運算(即僅保留Hash二進制的後i位元)。當ReHash時,數組長度加倍變為2i+1,舊數組第j項鍊表中的每個節點,要么移動到新數組中第j項,要么移動到新數組中第j+2i項,而它們的唯一差異在於Hash值第i+1位的不同(第i+1位為0則仍為第j項,否則為第j+2i項)。

java無鎖hashmap原理與實作詳解

如图5,我们将所有节点按照Hash值的翻转位序(如1101->1011)由小到大排列。当数组大小为8时,2、18在一个组内;3、 11、27在另一个组内。每组的开始,插入一个哨兵节点,以方便后续操作。为了使哨兵节点正确排在组的最前方,我们将正常节点Hash的最高位(翻转后变为最低位)置为1,而哨兵节点不设置这一位。

当数组扩容至16时(见图6),第二组分裂为一个只含3的组和一个含有11、27的组,但节点之间的相对顺序并未改变。这样在ReHash时,我们就不需要移动节点了。

java無鎖hashmap原理與實作詳解

实现细节

由于扩容时数组的复制会占用大量的时间,这里我们采用了将整个数组分块,懒惰建立的方法。这样,当访问到某下标时,仅需判断此下标所在块是否已建立完毕(如果没有则建立)。

另外定义size为当前已使用的下标范围,其初始值为2,数组扩容时仅需将size加倍即可;定义count代表目前HashMap中包含的总节点个数(不算哨兵节点)。

初始时,数组中除第0项外,所有项都为null。第0项指向一个仅有一个哨兵节点的链表,代表整条链的起点。初始时全貌见图7,其中浅绿色代表当前未使用的下标范围,虚线箭头代表逻辑上存在,但实际未建立的块。

初始化下标操作

数组中为null的项都认为处于未初始化状态,初始化某个下标即代表建立其对应的哨兵节点。初始化是递归进行的,即若其父下标未初始化,则先初始化其父下标。(一个下标的父下标是其移除最高二进制位后得到的下标)大致代码如下:

privatevoidinitializeBucket(intbucketIdx) { 

    intparentIdx = bucketIdx ^ Integer.highestOneBit(bucketIdx); 

    if(getBucket(parentIdx) ==null) 

        initializeBucket(parentIdx); 

    Node dummy =newNode(); 

    dummy.hash = Integer.reverse(bucketIdx); 

    dummy.next =newAtomicReference<>(); 

    setBucket(bucketIdx, listInsert(getBucket(parentIdx), dummy)); 

 
}

其中getBucket即封装过的获取数组某下标内容的方法,setBucket同理。listInsert将从指定位置开始查找适合插入的位置插入给定的节点,若链表中已存在hash相同的节点则返回那个已存在的节点;否则返回新插入的节点。

插入操作

首先用HashMap的size对键的hashCode取模,得到应插入的数组下标。
然后判断该下标处是否为null,如果为null则初始化此下标。
构造一个新的节点,并插入到适当位置,注意节点中的hash值应为原hashCode经过位翻转并将最低位置1之后的值。
将节点个数计数器加1,若加1后节点过多,则仅需将size改为size*2,代表对数组扩容(ReHash)。

查找操作

找出待查找节点在数组中的下标。
判断该下标处是否为null,如果为null则返回查找失败。
从相应位置进入链表,顺次寻找,直至找出待查找节点或超出本组节点范围。

删除操作

找出应删除节点在数组中的下标。
判断该下标处是否为null,如果为null则初始化此下标。
找到待删除节点,并从链表中删除。(注意由于哨兵节点的存在,任何正常元素只被其唯一的前驱节点所引用,不存在被前驱节点与数组中指针同时引用的情况,从而不会出现需要同时修改多个指针的情况)
将节点个数计数器减1。

更多java無鎖hashmap原理與實作詳解相关文章请关注PHP中文网!


陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
如何將Maven或Gradle用於高級Java項目管理,構建自動化和依賴性解決方案?如何將Maven或Gradle用於高級Java項目管理,構建自動化和依賴性解決方案?Mar 17, 2025 pm 05:46 PM

本文討論了使用Maven和Gradle進行Java項目管理,構建自動化和依賴性解決方案,以比較其方法和優化策略。

如何使用適當的版本控制和依賴項管理創建和使用自定義Java庫(JAR文件)?如何使用適當的版本控制和依賴項管理創建和使用自定義Java庫(JAR文件)?Mar 17, 2025 pm 05:45 PM

本文使用Maven和Gradle之類的工具討論了具有適當的版本控制和依賴關係管理的自定義Java庫(JAR文件)的創建和使用。

如何使用咖啡因或Guava Cache等庫在Java應用程序中實現多層緩存?如何使用咖啡因或Guava Cache等庫在Java應用程序中實現多層緩存?Mar 17, 2025 pm 05:44 PM

本文討論了使用咖啡因和Guava緩存在Java中實施多層緩存以提高應用程序性能。它涵蓋設置,集成和績效優勢,以及配置和驅逐政策管理最佳PRA

如何將JPA(Java持久性API)用於具有高級功能(例如緩存和懶惰加載)的對象相關映射?如何將JPA(Java持久性API)用於具有高級功能(例如緩存和懶惰加載)的對象相關映射?Mar 17, 2025 pm 05:43 PM

本文討論了使用JPA進行對象相關映射,並具有高級功能,例如緩存和懶惰加載。它涵蓋了設置,實體映射和優化性能的最佳實踐,同時突出潛在的陷阱。[159個字符]

Java的類負載機制如何起作用,包括不同的類載荷及其委託模型?Java的類負載機制如何起作用,包括不同的類載荷及其委託模型?Mar 17, 2025 pm 05:35 PM

Java的類上載涉及使用帶有引導,擴展程序和應用程序類負載器的分層系統加載,鏈接和初始化類。父代授權模型確保首先加載核心類別,從而影響自定義類LOA

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
4 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
4 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
1 個月前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.聊天命令以及如何使用它們
1 個月前By尊渡假赌尊渡假赌尊渡假赌

熱工具

MantisBT

MantisBT

Mantis是一個易於部署的基於Web的缺陷追蹤工具,用於幫助產品缺陷追蹤。它需要PHP、MySQL和一個Web伺服器。請查看我們的演示和託管服務。

SAP NetWeaver Server Adapter for Eclipse

SAP NetWeaver Server Adapter for Eclipse

將Eclipse與SAP NetWeaver應用伺服器整合。

VSCode Windows 64位元 下載

VSCode Windows 64位元 下載

微軟推出的免費、功能強大的一款IDE編輯器

SublimeText3 英文版

SublimeText3 英文版

推薦:為Win版本,支援程式碼提示!

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

強大的PHP整合開發環境