日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程语言 > java >内容正文

java

小马源码_Java互联网架构-重新认识Java8-HashMap-不一样的源码解读

發(fā)布時(shí)間:2025/3/8 java 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 小马源码_Java互联网架构-重新认识Java8-HashMap-不一样的源码解读 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

歡迎關(guān)注頭條號(hào):java小馬哥

周一至周日早九點(diǎn)半!下午三點(diǎn)半!精品技術(shù)文章準(zhǔn)時(shí)送上!!!

精品學(xué)習(xí)資料獲取通道,參見文末

看源碼前我們必須先知道一下ConcurrentHashMap的基本結(jié)構(gòu)。ConcurrentHashMap是采用分段鎖來進(jìn)行并發(fā)控制的。

其中有一個(gè)內(nèi)部類為Segment類用來表示鎖。而Segment類里又有一個(gè)HashEntry[]數(shù)組,這個(gè)數(shù)組才是真正用

來存放我們的key-value的。

大概為如下圖結(jié)構(gòu)。一個(gè)Segment數(shù)組,而Segment數(shù)組每個(gè)元素為一個(gè)HashEntry數(shù)組

看源碼前我們還必須了解的幾個(gè)默認(rèn)的常量值:

DEFAULT_INITIAL_CAPACITY = 16 容器默認(rèn)容量為16

DEFAULT_LOAD_FACTOR = 0.75f 默認(rèn)擴(kuò)容因子是0.75

DEFAULT_CONCURRENCY_LEVEL = 16 默認(rèn)并發(fā)度是16

MAXIMUM_CAPACITY = 1 << 30 容器最大容量為1073741824

MIN_SEGMENT_TABLE_CAPACITY = 2 段的最小大小

MAX_SEGMENTS = 1 << 16 段的最大大小

RETRIES_BEFORE_LOCK = 2 通過不獲取鎖的方式嘗試獲取size的次數(shù)

以上以及默認(rèn)值是ConcurrentHashMap中定義好的,下面我們很多地方會(huì)用到他們。

先從初始化開始說起

通過我們使用ConcurrentHashMap都是通過 ConcurrentHashMap map = new ConcurrentHashMap<>();的方式

我們點(diǎn)進(jìn)去跟蹤下源碼

/**

* Creates a new, empty map with a default initial capacity (16),

* load factor (0.75) and concurrencyLevel (16).

*/

public ConcurrentHashMap() {

this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);

}

可以看到,默認(rèn)無參構(gòu)造函數(shù)內(nèi)調(diào)用了另一個(gè)帶參構(gòu)造函數(shù),而這個(gè)構(gòu)造函數(shù)也就是不管你初始化時(shí)傳進(jìn)來什么參數(shù),最終都會(huì)跳到那個(gè)帶參構(gòu)造函數(shù)。

點(diǎn)進(jìn)去看看這個(gè)帶參構(gòu)造函數(shù)實(shí)現(xiàn)了什么功能

public ConcurrentHashMap(int initialCapacity,

float loadFactor, int concurrencyLevel) {

if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)

throw new IllegalArgumentException();

if (concurrencyLevel > MAX_SEGMENTS)

concurrencyLevel = MAX_SEGMENTS;

// Find power-of-two sizes best matching arguments

int sshift = 0;

int ssize = 1;

while (ssize < concurrencyLevel) {

++sshift;

ssize <<= 1;

}

this.segmentShift = 32 - sshift;

this.segmentMask = ssize - 1;

if (initialCapacity > MAXIMUM_CAPACITY)

initialCapacity = MAXIMUM_CAPACITY;

int c = initialCapacity / ssize;

if (c * ssize < initialCapacity)

++c;

int cap = MIN_SEGMENT_TABLE_CAPACITY;

while (cap < c)

cap <<= 1;

// create segments and segments[0]

Segment s0 =

new Segment(loadFactor, (int)(cap * loadFactor),

(HashEntry[])new HashEntry[cap]);

Segment[] ss = (Segment[])new Segment[ssize];

UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]

this.segments = ss;

}

我們看到該構(gòu)造函數(shù)一共有三個(gè)參數(shù),分別是容器的初始化大小、負(fù)載因子、并發(fā)度,這三個(gè)參數(shù)如果我們new 一個(gè)ConcurrentHashMap時(shí)沒有指定,

那么將會(huì)采用默認(rèn)的參數(shù),也就是我們本文開始說的那幾個(gè)常量值。

在這里我對(duì)這三個(gè)參數(shù)做下解釋。容器初始化大小是整個(gè)map的容量。負(fù)載因子是用來計(jì)算每個(gè)segment里的HashEntry數(shù)組擴(kuò)容時(shí)的閾值的。并發(fā)度是

用來設(shè)置segment數(shù)組的長(zhǎng)度的。

開頭這兩個(gè)if沒什么好說的。就是用來判斷我們傳進(jìn)來的參數(shù)的正確性。當(dāng)負(fù)載因子,初始容量和并發(fā)度不按照規(guī)范來時(shí)會(huì)拋出算術(shù)異常。第二個(gè)if時(shí)當(dāng)傳進(jìn)來的

并發(fā)度大于最大段大小的時(shí)候,就將其設(shè)置為最大段大小。

這段就比較有意思了。由于segment數(shù)組要求長(zhǎng)度必須為2的n次方,當(dāng)我們傳進(jìn)來的并發(fā)度不是2的n次方時(shí)會(huì)計(jì)算出一個(gè)最接近它的2的n次方值

比如如何我們傳進(jìn)來的并發(fā)度為14 15那么通過計(jì)算segment數(shù)組長(zhǎng)度就是16。在上圖中我們可以看到兩個(gè)局部變量ssize和sshift,在循環(huán)中如果ssize小于

并發(fā)度就將其二進(jìn)制左移一位,即乘2。因此ssize就是用來保存我們計(jì)算出來的最接近并發(fā)度的2的n次方值。而ssfhit是用來計(jì)算偏移量的。在這里我們又

要說兩個(gè)很重要的全局常量。segmentMask和segmentShift。其中segmentMask為ssize - 1,由于ssize為2的倍數(shù)。那么segmentMask就是奇數(shù)。化為

二進(jìn)制就是全1,而segmentShift為32 - sshift大小。32是key值經(jīng)過再hash求出來的值的二進(jìn)制位。segmentMask和segmentShift是用來定位當(dāng)前元素

在segment數(shù)組那個(gè)位置,和在HashEntry數(shù)組的哪個(gè)位置,后面我們會(huì)詳細(xì)說說怎么算的。

這一段代碼就是用來確定每個(gè)segment里面的hashentry的一些參數(shù)和初始化segment數(shù)組了。第一個(gè)if是防止我們?cè)O(shè)置的初始化

容量大于最大容量。而c是用來計(jì)算每個(gè)hashentry數(shù)組的容量。由于每個(gè)hashentry數(shù)組容量也需要為2的n次方,因此這里也需要

一個(gè)cap和循環(huán)來計(jì)算一個(gè)2的n次方值,方法和上面一樣。這里計(jì)算出來的cap值就是最終hashentry數(shù)組實(shí)際的大小了。

初始化就做了這些工作了。

那么我們?cè)谡f說最簡(jiǎn)單的get方法。

get方法就需要用到定位我們的元素了。而定位元素就需要我們上面初始化時(shí)設(shè)置好的兩個(gè)值:segmentMask和segmentShift

上面說了,并發(fā)度默認(rèn)值為16,那么ssize也為16,因此segmentMask為15.由于ssize二進(jìn)制往左移了4位,那么sshift就是4,

segmentShift就是32-4=28.下面我們就用segmentMask=15,segmentShift為28來說說怎么確定元素位置的。

在這里我們要說下hash值,這里的hash值不是key的hashcode值,而是經(jīng)過再hash確定下來的一個(gè)hash值,目的是為了減少hash沖突。

hash值二進(jìn)制為32位。

上圖兩個(gè)紅框就是分別確定segment數(shù)組中的位置和hashentry數(shù)組中的位置。

我們可以看到確定segment數(shù)組是采用 (h >>> segmentShift) & segmentMask,其中h為再hash過的hash值。將32為的hash值往右移segmentShift位。這里我們假設(shè)移了28位。

而segmentMask為15,就是4位都為一的二進(jìn)制。將高4位與segmentMask相與會(huì)等到一個(gè)小于16的值,就是當(dāng)前元素再的segment位置。

確定了所屬的segment后。就要確認(rèn)在的hashentry位置了。通過第二個(gè)紅框處,我們可以看到確定hashentry的位置沒有使用上面兩個(gè)值了。而是直接使用當(dāng)前hashentry數(shù)組的長(zhǎng)度減一

和hash值想與。通過兩種不同的算法分別定位segment和hashenrty可以保證元素在segment數(shù)組和hashentry數(shù)組里面都散列開了。

Put方法

public V put(K key, V value) {

Segment s;

if (value == null)

throw new NullPointerException();

int hash = hash(key);

int j = (hash >>> segmentShift) & segmentMask;

if ((s = (Segment)UNSAFE.getObject // nonvolatile; recheck

(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment

s = ensureSegment(j);

return s.put(key, hash, value, false);

}

final V put(K key, int hash, V value, boolean onlyIfAbsent) {

HashEntry node = tryLock() ? null :

scanAndLockForPut(key, hash, value);

V oldValue;

try {

HashEntry[] tab = table;

int index = (tab.length - 1) & hash;

HashEntry first = entryAt(tab, index);

for (HashEntry e = first;;) {

if (e != null) {

K k;

if ((k = e.key) == key ||

(e.hash == hash && key.equals(k))) {

oldValue = e.value;

if (!onlyIfAbsent) {

e.value = value;

++modCount;

}

break;

}

e = e.next;

}

else {

if (node != null)

node.setNext(first);

else

node = new HashEntry(hash, key, value, first);

int c = count + 1;

if (c > threshold && tab.length < MAXIMUM_CAPACITY)

rehash(node);

else

setEntryAt(tab, index, node);

++modCount;

count = c;

oldValue = null;

break;

}

}

} finally {

unlock();

}

return oldValue;

}

上面兩片代碼就是put一個(gè)元素的過程。由于Put方法里需要對(duì)共享變量進(jìn)行寫入操作,因此為了安全,需要在操作共享變量時(shí)加鎖。put時(shí)先定位到segment,然后在segment里及逆行擦汗如操作。

插入有兩個(gè)步驟,第一步判斷是否需要對(duì)segment里的hashenrty數(shù)組進(jìn)行擴(kuò)容。第二步是定位添加元素的位置,然后將其放在hashenrty數(shù)組里。

我們先說說擴(kuò)容。

在插入元素的時(shí)候會(huì)先判斷segment里面的hashenrty數(shù)組是否超過容量threshold。這個(gè)容量是我們剛開始初始化hashenrty數(shù)組時(shí)采用容量大小和負(fù)載因子計(jì)算出來的。

如果超過這個(gè)閾值(threshold)那么就會(huì)進(jìn)行擴(kuò)容。擴(kuò)容括的時(shí)當(dāng)前hashenrty而不是整個(gè)map。

如何擴(kuò)容

擴(kuò)容的時(shí)候會(huì)先創(chuàng)建一個(gè)容量是原來兩個(gè)容量大小的數(shù)組,然后將原數(shù)組里的元素進(jìn)行再散列后插入到新的數(shù)組里。

Size方法

由于map里的元素是遍布所有hashenrty的。因此統(tǒng)計(jì)size的時(shí)候需要統(tǒng)計(jì)每個(gè)hashenrty的大小。由于是并發(fā)環(huán)境下,可能出現(xiàn)有線程在插入或者刪除的情況。因此會(huì)出現(xiàn)

錯(cuò)誤。我們能想到的就是使用size方法時(shí)把所有的segment的put,remove和clean方法都鎖起來。但是這種方法時(shí)很低效的。因此concurrenthashmap采用了以下辦法:

先嘗試2次通過不加鎖的方式來統(tǒng)計(jì)各個(gè)segment大小,如果統(tǒng)計(jì)的過程中,容器的count發(fā)生了變化,再采用加鎖的方式來統(tǒng)計(jì)所有segment的大小。

concurrenthashmap時(shí)使用modcount變量來判斷再統(tǒng)計(jì)的時(shí)候容器是否放生了變化。在put、remove、clean方法里操作數(shù)據(jù)前都會(huì)將辯能力modCount進(jìn)行加一,那么在統(tǒng)計(jì)

size千后比較modCount是否發(fā)生變化,就可以知道容器大小是否發(fā)生變化了。

封面圖源網(wǎng)絡(luò),侵權(quán)刪除)

私信頭條號(hào),發(fā)送:“資料”,獲取更多“秘制” 精品學(xué)習(xí)資料

如有收獲,請(qǐng)幫忙轉(zhuǎn)發(fā),您的鼓勵(lì)是作者最大的動(dòng)力,謝謝!

一大波微服務(wù)、分布式、高并發(fā)、高可用的原創(chuàng)系列文章正在路上,

歡迎關(guān)注頭條號(hào):java小馬哥

周一至周日早九點(diǎn)半!下午三點(diǎn)半!精品技術(shù)文章準(zhǔn)時(shí)送上!!!

十余年BAT架構(gòu)經(jīng)驗(yàn)傾囊相授

總結(jié)

以上是生活随笔為你收集整理的小马源码_Java互联网架构-重新认识Java8-HashMap-不一样的源码解读的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。