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

Java中的ConcurrentHashMap集合源碼解析

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

前言

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

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

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

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

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數組。
		for (Node<K,V>[] tab = table;;) {
			Node<K,V> f; int n, i, fh;
			//如果數組為空,則進行數組的初始化。這里相當于懶漢模式,使用的時候才去初始化
			if (tab == null || (n = tab.length) == 0)
				//進行數組的初始化
				tab = initTable();
			//根據key的hash計算出該key在Node數組中的位置,并判斷這個位置是否為null
			else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
				//當前的數組位置為null,則使用CAS插入一個新的Node
				if (casTabAt(tab, i, null,
						new Node<K,V>(hash, key, value, null)))
					break;                   // no lock when adding to empty bin
			}
			//表示正在進行擴容操作 涉及了ForwardingNode 這個特殊的Node,
                        //在擴容時會進行創(chuàng)建,且固定傳入的hash值為 -1
			else if ((fh = f.hash) == MOVED)
				tab = helpTransfer(tab, f);
			//到了這里表示,出現(xiàn)了hash沖突,key在Node數組中的索引位置不為null。
			//需進行鏈表或紅黑樹的插入操作。
			else {
				V oldVal = null;
				//這個 f 存放是 根據key在數組中找到的Node,相當于紅黑樹或鏈表的頭結點,并進行加鎖
				synchronized (f) {
					//這里再進行一次判斷,保證f沒被其它線程修改
					if (tabAt(tab, i) == f) {
						//如果是鏈表
						if (fh >= 0) {
							//統(tǒng)計鏈表的長度
							binCount = 1;
							//對f這個Node進行累加鏈表的長度,并遍歷鏈表
							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;
							//使用紅黑樹的方式進行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,是則轉紅黑樹
					if (binCount >= TREEIFY_THRESHOLD)
						treeifyBin(tab, i);
					if (oldVal != null)
						return oldVal;
					break;
				}
			}
		}
		//統(tǒng)計整個的數組長度,并判斷是否需要擴容
		addCount(1L, binCount);
		return null;
	}

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

初始化數組

private final Node<K,V>[] initTable() {
		Node<K,V>[] tab; int sc;
		while ((tab = table) == null || tab.length == 0) {
			//表示已經有線程在進行初始化操作,讓出CPU的執(zhí)行權
			if ((sc = sizeCtl) < 0)
				Thread.yield(); // lost initialization race; just spin
			//把sc賦值為 -1,表示當前線程開始執(zhí)行初始化操作
			else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
				try {
					if ((tab = table) == null || tab.length == 0) {
						//獲取數組的初始長度,默認為16
						int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
						@SuppressWarnings("unchecked")
						//初始化數組
						Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
						table = tab = nt;
						sc = n - (n >>> 2);
					}
				} finally {
					//sizeCtl 參數 第一個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;
		//計算key的hash
		int h = spread(key.hashCode());
		if ((tab = table) != null && (n = tab.length) > 0 &&
				//根據hash值做運算獲取數組對應的Node(相當于頭結點)
				(e = tabAt(tab, (n - 1) & h)) != null) {
			//根據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;
			}
			//這里判斷是否正在擴容 或者 該節(jié)點為紅黑樹
			else if (eh < 0)
				return (p = e.find(h, key)) != null ? p.val : null;
			//到了這一步表示該結構為鏈表,遍歷鏈表,返回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大致是怎么實現(xiàn)的,區(qū)別相當大。

1.8之前的ConcurrentHashMap是采用了ReentrantLock+Segment+HashEntry的結構

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

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

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

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

相關文章

最新評論