輕松理解 Java HashMap 和 ConcurrentHashMap

前言

Map 這樣的Key Value在軟件開發(fā)中是非常經(jīng)典的結(jié)構(gòu)澳眷,常用于在內(nèi)存中存放數(shù)據(jù)。

本篇主要想討論 ConcurrentHashMap 這樣一個并發(fā)容器秉扑,在正式開始之前我覺得有必要談?wù)?HashMap寞缝,沒有它就不會有后面的 ConcurrentHashMap。

HashMap

眾所周知 HashMap 底層是基于數(shù)組 + 鏈表組成的嫡霞,不過在 jdk1.7 和 1.8 中具體實現(xiàn)稍有不同。

Base 1.7

1.7 中的數(shù)據(jù)結(jié)構(gòu)圖:

先來看看 1.7 中的實現(xiàn)希柿。

這是 HashMap 中比較核心的幾個成員變量诊沪;看看分別是什么意思养筒?

初始化桶大小,因為底層是數(shù)組端姚,所以這是數(shù)組默認(rèn)的大小晕粪。

桶最大值。

默認(rèn)的負(fù)載因子(0.75)

table真正存放數(shù)據(jù)的數(shù)組渐裸。

Map存放數(shù)量的大小巫湘。

桶大小,可在初始化時顯式指定昏鹃。

負(fù)載因子尚氛,可在初始化時顯式指定。

重點解釋下負(fù)載因子:

由于給定的 HashMap 的容量大小是固定的洞渤,比如默認(rèn)初始化:

public HashMap() {

? ? this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);

}

public HashMap(int initialCapacity, float loadFactor) {

? ? if (initialCapacity < 0)

? ? ? ? throw new IllegalArgumentException("Illegal initial capacity: " +

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? initialCapacity);

? ? if (initialCapacity > MAXIMUM_CAPACITY)

? ? ? ? initialCapacity = MAXIMUM_CAPACITY;

? ? if (loadFactor <= 0 || Float.isNaN(loadFactor))

? ? ? ? throw new IllegalArgumentException("Illegal load factor: " +

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? loadFactor);

? ? this.loadFactor = loadFactor;

? ? threshold = initialCapacity;

? ? init();

}

給定的默認(rèn)容量為 16阅嘶,負(fù)載因子為 0.75。Map 在使用過程中不斷的往里面存放數(shù)據(jù)载迄,當(dāng)數(shù)量達(dá)到了16 * 0.75 = 12就需要將當(dāng)前 16 的容量進(jìn)行擴容讯柔,而擴容這個過程涉及到 rehash、復(fù)制數(shù)據(jù)等操作护昧,所以非常消耗性能磷杏。

因此通常建議能提前預(yù)估 HashMap 的大小最好,盡量的減少擴容帶來的性能損耗捏卓。

根據(jù)代碼可以看到其實真正存放數(shù)據(jù)的是

transient Entry[] table = (Entry[]) EMPTY_TABLE;

這個數(shù)組,那么它又是如何定義的呢慈格?

Entry 是 HashMap 中的一個內(nèi)部類怠晴,從他的成員變量很容易看出:

key 就是寫入時的鍵。

value 自然就是值浴捆。

開始的時候就提到 HashMap 是由數(shù)組和鏈表組成蒜田,所以這個 next 就是用于實現(xiàn)鏈表結(jié)構(gòu)。

hash 存放的是當(dāng)前 key 的 hashcode选泻。

知曉了基本結(jié)構(gòu)冲粤,那來看看其中重要的寫入、獲取函數(shù):

put 方法

public V put(K key, V value) {

? ? if (table == EMPTY_TABLE) {

? ? ? ? inflateTable(threshold);

? ? }

? ? if (key == null)

? ? ? ? return putForNullKey(value);

? ? int hash = hash(key);

? ? int i = indexFor(hash, table.length);

? ? for (Entry e = table[i]; e != null; e = e.next) {

? ? ? ? Object k;

? ? ? ? if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

? ? ? ? ? ? V oldValue = e.value;

? ? ? ? ? ? e.value = value;

? ? ? ? ? ? e.recordAccess(this);

? ? ? ? ? ? return oldValue;

? ? ? ? }

? ? }

? ? modCount++;

? ? addEntry(hash, key, value, i);

? ? return null;

}

判斷當(dāng)前數(shù)組是否需要初始化页眯。

如果 key 為空梯捕,則 put 一個空值進(jìn)去。

根據(jù) key 計算出 hashcode窝撵。

根據(jù)計算出的 hashcode 定位出所在桶傀顾。

如果桶是一個鏈表則需要遍歷判斷里面的 hashcode、key 是否和傳入 key 相等碌奉,如果相等則進(jìn)行覆蓋短曾,并返回原來的值寒砖。

如果桶是空的,說明當(dāng)前位置沒有數(shù)據(jù)存入嫉拐;新增一個 Entry 對象寫入當(dāng)前位置哩都。

void addEntry(int hash, K key, V value, int bucketIndex) {

? ? if ((size >= threshold) && (null != table[bucketIndex])) {

? ? ? ? resize(2 * table.length);

? ? ? ? hash = (null != key) ? hash(key) : 0;

? ? ? ? bucketIndex = indexFor(hash, table.length);

? ? }

? ? createEntry(hash, key, value, bucketIndex);

}

void createEntry(int hash, K key, V value, int bucketIndex) {

? ? Entry e = table[bucketIndex];

? ? table[bucketIndex] = new Entry<>(hash, key, value, e);

? ? size++;

}

當(dāng)調(diào)用 addEntry 寫入 Entry 時需要判斷是否需要擴容。

如果需要就進(jìn)行兩倍擴充婉徘,并將當(dāng)前的 key 重新 hash 并定位漠嵌。

而在createEntry中會將當(dāng)前位置的桶傳入到新建的桶中,如果當(dāng)前桶有值就會在位置形成鏈表判哥。

get 方法

再來看看 get 函數(shù):

public V get(Object key) {

? ? if (key == null)

? ? ? ? return getForNullKey();

? ? Entry entry = getEntry(key);

? ? return null == entry ? null : entry.getValue();

}

final Entry getEntry(Object key) {

? ? if (size == 0) {

? ? ? ? return null;

? ? }

? ? int hash = (key == null) ? 0 : hash(key);

? ? for (Entry e = table[indexFor(hash, table.length)];

? ? ? ? e != null;

? ? ? ? e = e.next) {

? ? ? ? Object k;

? ? ? ? if (e.hash == hash &&

? ? ? ? ? ? ((k = e.key) == key || (key != null && key.equals(k))))

? ? ? ? ? ? return e;

? ? }

? ? return null;

}

首先也是根據(jù) key 計算出 hashcode献雅,然后定位到具體的桶中。

判斷該位置是否為鏈表塌计。

不是鏈表就根據(jù)key挺身、key 的 hashcode是否相等來返回值。

為鏈表則需要遍歷直到 key 及 hashcode 相等時候就返回值锌仅。

啥都沒取到就直接返回 null 章钾。

Base 1.8

不知道 1.7 的實現(xiàn)大家看出需要優(yōu)化的點沒有?

其實一個很明顯的地方就是:

當(dāng) Hash 沖突嚴(yán)重時热芹,在桶上形成的鏈表會變的越來越長贱傀,這樣在查詢時的效率就會越來越低;時間復(fù)雜度為O(N)伊脓。

因此 1.8 中重點優(yōu)化了這個查詢效率府寒。

1.8 HashMap 結(jié)構(gòu)圖:

先來看看幾個核心的成員變量:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**

* The maximum capacity, used if a higher value is implicitly specified

* by either of the constructors with arguments.

* MUST be a power of two <= 1<<30.

*/

static final int MAXIMUM_CAPACITY = 1 << 30;

/**

* The load factor used when none specified in constructor.

*/

static final float DEFAULT_LOAD_FACTOR = 0.75f;

static final int TREEIFY_THRESHOLD = 8;

transient Node[] table;

/**

* Holds cached entrySet(). Note that AbstractMap fields are used

* for keySet() and values().

*/

transient Set> entrySet;

/**

* The number of key-value mappings contained in this map.

*/

transient int size;

和 1.7 大體上都差不多,還是有幾個重要的區(qū)別:

TREEIFY_THRESHOLD用于判斷是否需要將鏈表轉(zhuǎn)換為紅黑樹的閾值报腔。

HashEntry 修改為 Node株搔。

Node 的核心組成其實也是和 1.7 中的 HashEntry 一樣,存放的都是key value hashcode next等數(shù)據(jù)纯蛾。

再來看看核心方法纤房。

put 方法

看似要比 1.7 的復(fù)雜,我們一步步拆解:

判斷當(dāng)前桶是否為空翻诉,空的就需要初始化(resize 中會判斷是否進(jìn)行初始化)炮姨。

根據(jù)當(dāng)前 key 的 hashcode 定位到具體的桶中并判斷是否為空,為空表明沒有 Hash 沖突就直接在當(dāng)前位置創(chuàng)建一個新桶即可碰煌。

如果當(dāng)前桶有值( Hash 沖突)舒岸,那么就要比較當(dāng)前桶中的key、key 的 hashcode與寫入的 key 是否相等芦圾,相等就賦值給e,在第 8 步的時候會統(tǒng)一進(jìn)行賦值及返回吁津。

如果當(dāng)前桶為紅黑樹,那就要按照紅黑樹的方式寫入數(shù)據(jù)。

如果是個鏈表碍脏,就需要將當(dāng)前的 key梭依、value 封裝成一個新節(jié)點寫入到當(dāng)前桶的后面(形成鏈表)。

接著判斷當(dāng)前鏈表的大小是否大于預(yù)設(shè)的閾值典尾,大于時就要轉(zhuǎn)換為紅黑樹役拴。

如果在遍歷過程中找到 key 相同時直接退出遍歷。

如果e != null就相當(dāng)于存在相同的 key,那就需要將值覆蓋钾埂。

最后判斷是否需要進(jìn)行擴容河闰。

get 方法

public V get(Object key) {

? ? Node e;

? ? return (e = getNode(hash(key), key)) == null ? null : e.value;

}

final Node getNode(int hash, Object key) {

? ? Node[] tab; Node first, e; int n; K k;

? ? if ((tab = table) != null && (n = tab.length) > 0 &&

? ? ? ? (first = tab[(n - 1) & hash]) != null) {

? ? ? ? if (first.hash == hash && // always check first node

? ? ? ? ? ? ((k = first.key) == key || (key != null && key.equals(k))))

? ? ? ? ? ? return first;

? ? ? ? if ((e = first.next) != null) {

? ? ? ? ? ? if (first instanceof TreeNode)

? ? ? ? ? ? ? ? return ((TreeNode)first).getTreeNode(hash, key);

? ? ? ? ? ? do {

? ? ? ? ? ? ? ? if (e.hash == hash &&

? ? ? ? ? ? ? ? ? ? ((k = e.key) == key || (key != null && key.equals(k))))

? ? ? ? ? ? ? ? ? ? return e;

? ? ? ? ? ? } while ((e = e.next) != null);

? ? ? ? }

? ? }

? ? return null;

}

get 方法看起來就要簡單許多了。

首先將 key hash 之后取得所定位的桶褥紫。

如果桶為空則直接返回 null 姜性。

否則判斷桶的第一個位置(有可能是鏈表、紅黑樹)的 key 是否為查詢的 key髓考,是就直接返回 value部念。

如果第一個不匹配,則判斷它的下一個是紅黑樹還是鏈表氨菇。

紅黑樹就按照樹的查找方式返回值儡炼。

不然就按照鏈表的方式遍歷匹配返回值。

從這兩個核心方法(get/put)可以看出 1.8 中對大鏈表做了優(yōu)化查蓉,修改為紅黑樹之后查詢效率直接提高到了O(logn)乌询。

但是 HashMap 原有的問題也都存在,比如在并發(fā)場景下使用時容易出現(xiàn)死循環(huán)豌研。

final HashMap map = new HashMap();

for (int i = 0; i < 1000; i++) {

? ? new Thread(new Runnable() {

? ? ? ? @Override

? ? ? ? public void run() {

? ? ? ? ? ? map.put(UUID.randomUUID().toString(), "");

? ? ? ? }

? ? }).start();

}

但是為什么呢妹田?簡單分析下。

看過上文的還記得在 HashMap 擴容的時候會調(diào)用resize()方法鹃共,就是這里的并發(fā)操作容易在一個桶上形成環(huán)形鏈表秆麸;這樣當(dāng)獲取一個不存在的 key 時,計算出的 index 正好是環(huán)形鏈表的下標(biāo)就會出現(xiàn)死循環(huán)及汉。

如下圖:

遍歷方式

還有一個值得注意的是 HashMap 的遍歷方式,通常有以下幾種:

Iterator> entryIterator = map.entrySet().iterator();

? ? ? ? while (entryIterator.hasNext()) {

? ? ? ? ? ? Map.Entry next = entryIterator.next();

? ? ? ? ? ? System.out.println("key=" + next.getKey() + " value=" + next.getValue());

? ? ? ? }

Iterator iterator = map.keySet().iterator();

? ? ? ? while (iterator.hasNext()){

? ? ? ? ? ? String key = iterator.next();

? ? ? ? ? ? System.out.println("key=" + key + " value=" + map.get(key));

? ? ? ? }

強烈建議使用第一種 EntrySet 進(jìn)行遍歷屯烦。

第一種可以把 key value 同時取出坷随,第二種還得需要通過 key 取一次 value,效率較低驻龟。

簡單總結(jié)下 HashMap:無論是 1.7 還是 1.8 其實都能看出 JDK 沒有對它做任何的同步操作温眉,所以并發(fā)會出問題,甚至 1.7 中出現(xiàn)死循環(huán)導(dǎo)致系統(tǒng)不可用(1.8 已經(jīng)修復(fù)死循環(huán)問題)翁狐。

因此 JDK 推出了專項專用的 ConcurrentHashMap 类溢,該類位于java.util.concurrent包下,專門用于解決并發(fā)問題。

堅持看到這里的朋友算是已經(jīng)把 ConcurrentHashMap 的基礎(chǔ)已經(jīng)打牢了闯冷,下面正式開始分析砂心。

ConcurrentHashMap

ConcurrentHashMap 同樣也分為 1.7 、1.8 版蛇耀,兩者在實現(xiàn)上略有不同辩诞。

Base 1.7

先來看看 1.7 的實現(xiàn),下面是他的結(jié)構(gòu)圖:

如圖所示纺涤,是由 Segment 數(shù)組译暂、HashEntry 組成,和 HashMap 一樣撩炊,仍然是數(shù)組加鏈表外永。

它的核心成員變量:

/**

* Segment 數(shù)組,存放數(shù)據(jù)時首先需要定位到具體的 Segment 中拧咳。

*/

final Segment[] segments;

transient Set keySet;

transient Set> entrySet;

Segment 是 ConcurrentHashMap 的一個內(nèi)部類伯顶,主要的組成如下:

static final class Segment extends ReentrantLock implements Serializable {

? ? ? private static final long serialVersionUID = 2249069246763182397L;

? ? ? // 和 HashMap 中的 HashEntry 作用一樣,真正存放數(shù)據(jù)的桶

? ? ? transient volatile HashEntry[] table;

? ? ? transient int count;

? ? ? transient int modCount;

? ? ? transient int threshold;

? ? ? final float loadFactor;

}

看看其中 HashEntry 的組成:

和 HashMap 非常類似呛踊,唯一的區(qū)別就是其中的核心數(shù)據(jù)如 value 砾淌,以及鏈表都是Volatile修飾的,保證了獲取時的可見性谭网。

原理上來說:ConcurrentHashMap 采用了分段鎖技術(shù)汪厨,其中 Segment 繼承于 ReentrantLock。不會像 HashTable 那樣不管是 put 還是 get 操作都需要做同步處理愉择,理論上 ConcurrentHashMap 支持 CurrencyLevel (Segment 數(shù)組數(shù)量)的線程并發(fā)劫乱。每當(dāng)一個線程占用鎖訪問一個 Segment 時,不會影響到其他的 Segment锥涕。

下面也來看看核心的put get方法衷戈。

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);

}

首先是通過 key 定位到 Segment,之后在對應(yīng)的 Segment 中進(jìn)行具體的 put层坠。

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;

}

雖然 HashEntry 中的 value 是用 volatile 關(guān)鍵詞修飾的殖妇,但是并不能保證并發(fā)的原子性,所以 put 操作時仍然需要加鎖處理破花。

首先第一步的時候會嘗試獲取鎖谦趣,如果獲取失敗肯定就有其他線程存在競爭,則利用scanAndLockForPut()自旋獲取鎖座每。

嘗試自旋獲取鎖前鹅。

如果重試的次數(shù)達(dá)到了MAX_SCAN_RETRIES則改為阻塞鎖獲取,保證能獲取成功峭梳。

再結(jié)合圖看看 put 的流程舰绘。

將當(dāng)前 Segment 中的 table 通過 key 的 hashcode 定位到 HashEntry。

遍歷該 HashEntry,如果不為空則判斷傳入的 key 和當(dāng)前遍歷的 key 是否相等捂寿,相等則覆蓋舊的 value口四。

不為空則需要新建一個 HashEntry 并加入到 Segment 中,同時會先判斷是否需要擴容者蠕。

最后會解除在 1 中所獲取當(dāng)前 Segment 的鎖窃祝。

get 方法

public V get(Object key) {

? ? Segment s; // manually integrate access methods to reduce overhead

? ? HashEntry[] tab;

? ? int h = hash(key);

? ? long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;

? ? if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null &&

? ? ? ? (tab = s.table) != null) {

? ? ? ? for (HashEntry e = (HashEntry) UNSAFE.getObjectVolatile

? ? ? ? ? ? ? ? (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);

? ? ? ? ? ? e != null; e = e.next) {

? ? ? ? ? ? K k;

? ? ? ? ? ? if ((k = e.key) == key || (e.hash == h && key.equals(k)))

? ? ? ? ? ? ? ? return e.value;

? ? ? ? }

? ? }

? ? return null;

}

get 邏輯比較簡單:

只需要將 Key 通過 Hash 之后定位到具體的 Segment ,再通過一次 Hash 定位到具體的元素上踱侣。

由于 HashEntry 中的 value 屬性是用 volatile 關(guān)鍵詞修飾的粪小,保證了內(nèi)存可見性,所以每次獲取時都是最新值抡句。

ConcurrentHashMap 的 get 方法是非常高效的探膊,因為整個過程都不需要加鎖

Base 1.8

1.7 已經(jīng)解決了并發(fā)問題待榔,并且能支持 N 個 Segment 這么多次數(shù)的并發(fā)逞壁,但依然存在 HashMap 在 1.7 版本中的問題。

那就是查詢遍歷鏈表效率太低锐锣。

因此 1.8 做了一些數(shù)據(jù)結(jié)構(gòu)上的調(diào)整腌闯。

首先來看下底層的組成結(jié)構(gòu):

看起來是不是和 1.8 HashMap 結(jié)構(gòu)類似?

其中拋棄了原有的 Segment 分段鎖雕憔,而采用了CAS + synchronized來保證并發(fā)安全性姿骏。

也將 1.7 中存放數(shù)據(jù)的 HashEntry 改為 Node,但作用都是相同的斤彼。

其中的val next都用了 volatile 修飾分瘦,保證了可見性。

put 方法

重點來看看 put 函數(shù):

根據(jù) key 計算出 hashcode 。

判斷是否需要進(jìn)行初始化。

f即為當(dāng)前 key 定位出的 Node除破,如果為空表示當(dāng)前位置可以寫入數(shù)據(jù),利用 CAS 嘗試寫入去团,失敗則自旋保證成功。

如果當(dāng)前位置的hashcode == MOVED == -1,則需要進(jìn)行擴容穷蛹。

如果都不滿足土陪,則利用 synchronized 鎖寫入數(shù)據(jù)。

如果數(shù)量大于TREEIFY_THRESHOLD則要轉(zhuǎn)換為紅黑樹俩莽。

get 方法

根據(jù)計算出來的 hashcode 尋址,如果就在桶上那么直接返回值乔遮。

如果是紅黑樹那就按照樹的方式獲取值扮超。

就不滿足那就按照鏈表的方式遍歷獲取值。

1.8 在 1.7 的數(shù)據(jù)結(jié)構(gòu)上做了大的改動,采用紅黑樹之后可以保證查詢效率(O(logn))出刷,甚至取消了 ReentrantLock 改為了 synchronized璧疗,這樣可以看出在新版的 JDK 中對 synchronized 優(yōu)化是很到位的。

在此我向大家推薦一個架構(gòu)學(xué)習(xí)交流群馁龟。交流學(xué)習(xí)群號:478030634 ?里面會分享一些資深架構(gòu)師錄制的視頻錄像:有Spring崩侠,MyBatis,Netty源碼分析坷檩,高并發(fā)却音、高性能、分布式矢炼、微服務(wù)架構(gòu)的原理系瓢,JVM性能優(yōu)化、分布式架構(gòu)等這些成為架構(gòu)師必備的知識體系句灌。還能領(lǐng)取免費的學(xué)習(xí)資源夷陋,目前受益良多

總結(jié)

看完了整個 HashMap 和 ConcurrentHashMap 在 1.7 和 1.8 中不同的實現(xiàn)方式相信大家對他們的理解應(yīng)該會更加到位。

其實這塊也是面試的重點內(nèi)容胰锌,通常的套路是:

談?wù)勀憷斫獾?HashMap骗绕,講講其中的 get put 過程。

1.8 做了什么優(yōu)化资昧?

是線程安全的嘛酬土?

不安全會導(dǎo)致哪些問題?

如何解決榛搔?有沒有線程安全的并發(fā)容器诺凡?

ConcurrentHashMap 是如何實現(xiàn)的? 1.7践惑、1.8 實現(xiàn)有何不同腹泌?為什么這么做?

這一串問題相信大家仔細(xì)看完都能懟回面試官尔觉。

除了面試會問到之外平時的應(yīng)用其實也蠻多凉袱,像之前談到的Guava 中 Cache的實現(xiàn)就是利用 ConcurrentHashMap 的思想。

同時也能學(xué)習(xí) JDK 作者大牛們的優(yōu)化思路以及并發(fā)解決方案侦铜。


大家覺得文章對你還是有一點點幫助的专甩,大家可以點擊下方二維碼進(jìn)行關(guān)注。 《Java爛豬皮》 公眾號聊的不僅僅是Java技術(shù)知識钉稍,還有面試等干貨涤躲,后期還有大量架構(gòu)干貨。大家一起關(guān)注吧贡未!關(guān)注爛豬皮种樱,你會了解的更多..............


其實寫這篇的前提是源于 GitHub 上的一個Issues蒙袍,也希望大家能參與進(jìn)來,共同維護好這個項目嫩挤。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末害幅,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子岂昭,更是在濱河造成了極大的恐慌以现,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件约啊,死亡現(xiàn)場離奇詭異邑遏,居然都是意外死亡,警方通過查閱死者的電腦和手機棍苹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門无宿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人枢里,你說我怎么就攤上這事孽鸡。” “怎么了栏豺?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵彬碱,是天一觀的道長。 經(jīng)常有香客問我奥洼,道長巷疼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任灵奖,我火速辦了婚禮嚼沿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘瓷患。我一直安慰自己骡尽,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布擅编。 她就那樣靜靜地躺著攀细,像睡著了一般。 火紅的嫁衣襯著肌膚如雪爱态。 梳的紋絲不亂的頭發(fā)上谭贪,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天,我揣著相機與錄音锦担,去河邊找鬼俭识。 笑死,一個胖子當(dāng)著我的面吹牛洞渔,可吹牛的內(nèi)容都是我干的套媚。 我是一名探鬼主播理盆,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼凑阶!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起衷快,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤宙橱,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蘸拔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體师郑,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年调窍,在試婚紗的時候發(fā)現(xiàn)自己被綠了宝冕。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡邓萨,死狀恐怖地梨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情缔恳,我是刑警寧澤宝剖,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站歉甚,受9級特大地震影響万细,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜纸泄,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一赖钞、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧聘裁,春花似錦雪营、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至砰诵,卻和暖如春征唬,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背茁彭。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工总寒, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人理肺。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓摄闸,卻偏偏與公主長得像善镰,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子年枕,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 1.HashMap是一個數(shù)組+鏈表/紅黑樹的結(jié)構(gòu)炫欺,數(shù)組的下標(biāo)在HashMap中稱為Bucket值,每個數(shù)組項對應(yīng)的...
    誰在烽煙彼岸閱讀 1,014評論 2 2
  • 轉(zhuǎn)載:https://www.cnblogs.com/xdouby/p/6026618.html 在JDK 1.4...
    境里婆娑閱讀 2,556評論 0 4
  • 前言 Map 這樣的 Key Value 在軟件開發(fā)中是非常經(jīng)典的結(jié)構(gòu),常用于在內(nèi)存中存放數(shù)據(jù)摩桶。 本篇主要想討論 ...
    Java小鋪閱讀 277評論 1 0
  • 想表達(dá)的大概就是題目的字面意思桥状,孤獨 無眠。從8月初到今天一直失眠硝清,家庭壓力辅斟、自卑心理、一個人的無助芦拿、同學(xué)友人面前...
    給自己講故事閱讀 304評論 0 0
  • 1. 很久不寫和愛情有關(guān)的東西了士飒。覺得矯情。 偶爾翻翻看過去寫的蔗崎,會好奇自己為什么把那么私人的情感放到網(wǎng)上变汪,還樂此...
    Elizen閱讀 329評論 0 3