欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Java中的ConcurrentHashMap集合源碼解析

 更新時間:2023年11月15日 11:07:28   作者:龍三丶  
這篇文章主要介紹了Java中的ConcurrentHashMap集合源碼解析,ConcurrentHashMap底層容器和HashMap相同,同樣是Node數(shù)組+鏈表+紅黑樹,不同的是在原來的基礎(chǔ)之上使用了Synchronized+CAS來保證線程安全,下面我們來進(jìn)行源碼分析,需要的朋友可以參考下

前言

在并發(fā)環(huán)境下,HashMap會出現(xiàn)線程安全問題,HashMap的擴(kuò)容操作會出現(xiàn)閉環(huán)現(xiàn)象,當(dāng)調(diào)用get方法時,會出現(xiàn)死循環(huán)。

所以,JDK給我們提供了另一個線程安全容器,ConcurrentHashMap。

在本章中我們來詳細(xì)探討JDK 1.8中ConcurrentHashMap的核心方法,且為什么是線程安全的以及和JDK 1.8之前的又有何區(qū)別。

ConcurrentHashMap底層容器和HashMap相同,同樣是Node數(shù)組+鏈表+紅黑樹,不同的是在原來的基礎(chǔ)之上使用了Synchronized+CAS來保證線程安全,下面我們來進(jìn)行源碼分析。

put

public V put(K key, V value) {
		return putVal(key, value, false);
	}
 
	final V putVal(K key, V value, boolean onlyIfAbsent) {
		//從這我們可以看出ConcurrentHashMap的key和value不能為null
		if (key == null || value == null) throw new NullPointerException();
		//得到key的hash值
		int hash = spread(key.hashCode());
		//這是用來記錄鏈表的長度
		int binCount = 0;
		//table:核心的Node數(shù)組。
		for (Node<K,V>[] tab = table;;) {
			Node<K,V> f; int n, i, fh;
			//如果數(shù)組為空,則進(jìn)行數(shù)組的初始化。這里相當(dāng)于懶漢模式,使用的時候才去初始化
			if (tab == null || (n = tab.length) == 0)
				//進(jìn)行數(shù)組的初始化
				tab = initTable();
			//根據(jù)key的hash計(jì)算出該key在Node數(shù)組中的位置,并判斷這個位置是否為null
			else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
				//當(dāng)前的數(shù)組位置為null,則使用CAS插入一個新的Node
				if (casTabAt(tab, i, null,
						new Node<K,V>(hash, key, value, null)))
					break;                   // no lock when adding to empty bin
			}
			//表示正在進(jìn)行擴(kuò)容操作 涉及了ForwardingNode 這個特殊的Node,
                        //在擴(kuò)容時會進(jìn)行創(chuàng)建,且固定傳入的hash值為 -1
			else if ((fh = f.hash) == MOVED)
				tab = helpTransfer(tab, f);
			//到了這里表示,出現(xiàn)了hash沖突,key在Node數(shù)組中的索引位置不為null。
			//需進(jìn)行鏈表或紅黑樹的插入操作。
			else {
				V oldVal = null;
				//這個 f 存放是 根據(jù)key在數(shù)組中找到的Node,相當(dāng)于紅黑樹或鏈表的頭結(jié)點(diǎn),并進(jìn)行加鎖
				synchronized (f) {
					//這里再進(jìn)行一次判斷,保證f沒被其它線程修改
					if (tabAt(tab, i) == f) {
						//如果是鏈表
						if (fh >= 0) {
							//統(tǒng)計(jì)鏈表的長度
							binCount = 1;
							//對f這個Node進(jìn)行累加鏈表的長度,并遍歷鏈表
							for (Node<K,V> e = f;; ++binCount) {
								K ek;
								//如果存在相同的value,則覆蓋
								if (e.hash == hash &&
										((ek = e.key) == key ||
												(ek != null && key.equals(ek)))) {
									oldVal = e.val;
									if (!onlyIfAbsent)
										e.val = value;
									break;
								}
								Node<K,V> pred = e;
								//遍歷到最后一個Node,插入一個新的Node到鏈表的尾部
								if ((e = e.next) == null) {
									pred.next = new Node<K,V>(hash, key,
											value, null);
									break;
								}
							}
						}
						//如果是紅黑樹
						else if (f instanceof TreeBin) {
							Node<K,V> p;
							binCount = 2;
							//使用紅黑樹的方式進(jìn)行Node的插入
							if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
									value)) != null) {
								oldVal = p.val;
								if (!onlyIfAbsent)
									p.val = value;
							}
						}
					}
				}
				if (binCount != 0) {
					//判斷鏈表的長度是否大于TREEIFY_THRESHOLD,是則轉(zhuǎn)紅黑樹
					if (binCount >= TREEIFY_THRESHOLD)
						treeifyBin(tab, i);
					if (oldVal != null)
						return oldVal;
					break;
				}
			}
		}
		//統(tǒng)計(jì)整個的數(shù)組長度,并判斷是否需要擴(kuò)容
		addCount(1L, binCount);
		return null;
	}

好了,以上就是大致的分析內(nèi)容,但還有許多步驟沒有展開代碼詳細(xì)說明,如初始化、鏈表轉(zhuǎn)紅黑樹及擴(kuò)容等,其中擴(kuò)容步驟非常復(fù)雜,有機(jī)會我會單獨(dú)寫一篇博客詳細(xì)介紹,在這就不多說了。我們接下來介紹較為簡單的初始化及get方法。

初始化數(shù)組

private final Node<K,V>[] initTable() {
		Node<K,V>[] tab; int sc;
		while ((tab = table) == null || tab.length == 0) {
			//表示已經(jīng)有線程在進(jìn)行初始化操作,讓出CPU的執(zhí)行權(quán)
			if ((sc = sizeCtl) < 0)
				Thread.yield(); // lost initialization race; just spin
			//把sc賦值為 -1,表示當(dāng)前線程開始執(zhí)行初始化操作
			else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
				try {
					if ((tab = table) == null || tab.length == 0) {
						//獲取數(shù)組的初始長度,默認(rèn)為16
						int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
						@SuppressWarnings("unchecked")
						//初始化數(shù)組
						Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
						table = tab = nt;
						sc = n - (n >>> 2);
					}
				} finally {
					//sizeCtl 參數(shù) 第一個if判斷需要用到
					sizeCtl = sc;
				}
				break;
			}
		}
		return tab;
	}

get

public V get(Object key) {
		Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
		//計(jì)算key的hash
		int h = spread(key.hashCode());
		if ((tab = table) != null && (n = tab.length) > 0 &&
				//根據(jù)hash值做運(yùn)算獲取數(shù)組對應(yīng)的Node(相當(dāng)于頭結(jié)點(diǎn))
				(e = tabAt(tab, (n - 1) & h)) != null) {
			//根據(jù)hash和equals判斷該Node的key是否和get的key相等,是則返回value
			if ((eh = e.hash) == h) {
				if ((ek = e.key) == key || (ek != null && key.equals(ek)))
					return e.val;
			}
			//這里判斷是否正在擴(kuò)容 或者 該節(jié)點(diǎn)為紅黑樹
			else if (eh < 0)
				return (p = e.find(h, key)) != null ? p.val : null;
			//到了這一步表示該結(jié)構(gòu)為鏈表,遍歷鏈表,返回value
			while ((e = e.next) != null) {
				if (e.hash == h &&
						((ek = e.key) == key || (ek != null && key.equals(ek))))
					return e.val;
			}
		}
		return null;
	}

好了,核心的源碼部分就分析到這里,我們在來看看JDK 1.8之前的ConcurrentHashMap大致是怎么實(shí)現(xiàn)的,區(qū)別相當(dāng)大。

1.8之前的ConcurrentHashMap是采用了ReentrantLock+Segment+HashEntry的結(jié)構(gòu)

整體就是由一個個 Segment 組成,Segment中包含了HashEntry數(shù)組,HashEntry數(shù)組就是1.8的那個table,只不過它這里是多個。

其中Segment在實(shí)現(xiàn)上繼承了ReentrantLock,這樣就自帶了鎖的功能,它在進(jìn)行put的時候只會鎖住一個Segment,所以理論上,最多可以同時支持 Segment 個數(shù)的線程并發(fā)寫,只要它們的操作分別分布在不同的 Segment 上。get的時候也是先找到一個Segment,然后在根據(jù)Hash值找到數(shù)組中的值。

至于為什么JDK在之后使用Synchronized來保證線程安全,是因?yàn)樵贘DK在版本迭代中一直在對Synchronized進(jìn)行優(yōu)化,使得Synchronized關(guān)鍵字在某些場景下已經(jīng)不比ReentrantLock效率慢,甚至更快。

到此這篇關(guān)于Java中的ConcurrentHashMap集合源碼解析的文章就介紹到這了,更多相關(guān)ConcurrentHashMap集合源碼內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論