生活随笔
收集整理的這篇文章主要介紹了
并发容器与框架——并发容器(一)
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
2019獨角獸企業重金招聘Python工程師標準>>>
Java中提供了非常多的并發容器和框架,在此僅對部分并發容器進行介紹。
1.ConcurrentHashMap的實現原理與使用 ? ? 1.1 為何使用ConcurrentHashMap ????在并發編程中使用HashMap可能導致程序死循環。而使用線程安全的HashTable效率又非 常低下,基于以上兩個原因,便有了ConcurrentHashMap。
線程不安全的HashMap:在多線程環境下,使用HashMap進行put操作會引起get操作死循環,導致CPU利用率接近100%,所以在并發情況下不能使用HashMap。HashMap在并發執行put操作時會引起死循環,是因為多線程會導致HashMap的Entry鏈表形成環形數據結構,一旦形成環形數據結構,Entry的next節點永遠不為空,就會產生死循環獲取Entry。 效率低下的HashTable:HashTable容器使用synchronized來保證線程安全,但在線程競爭激烈的情況下HashTable 的效率非常低下。因為當一個線程訪問HashTable的同步方法,其他線程也訪問HashTable的同 步方法時,會進入阻塞或輪詢狀態。如線程1使用put進行元素添加,線程2不但不能使用put方 法添加元素,也不能使用get方法來獲取元素,所以競爭越激烈效率越低。 ConcurrentHashMap的鎖分段技術:HashTable容器在競爭激烈的并發環境下表現出效率低下的原因是所有訪問HashTable的線程都必須競爭同一把鎖,假如容器里有多把鎖,每一把鎖用于鎖容器其中一部分數據,那么 當多線程訪問容器里不同數據段的數據時,線程間就不會存在鎖競爭,從而可以有效提高并發訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術。首先將數據分成一段一段地存儲,然后給每一段數據配一把鎖,當一個線程占用鎖訪問其中一個段數據的時候,其他段的數據也能被其他線程訪問。 ????1.2?ConcurrentHashMap結構 ????ConcurrentHashMap是由Segment數組結構和HashEntry數組結構組成。Segment是一種可重入鎖(ReentrantLock),在ConcurrentHashMap里扮演鎖的角色 ;HashEntry則用于存儲鍵值對數據 。一個ConcurrentHashMap里包含一個Segment數組 。Segment的結構和HashMap類似,是一種 數組和鏈表結構。 一個Segment里包含(用“鎖住”或許更合適)一個HashEntry數組 ,每個HashEntry數組中的元素是一個鏈表結構的元素 ,每個Segment守護著一個HashEntry數組里的元素,當對HashEntry數組的數據進行修改時,必須首先獲得與它對應的Segment鎖。也就是說一個ConcurrentHashMap里有著多個Segment數組,而Segment數組中的每個元素鎖住是HashEntry數組,每個HashEntry數組中的每個數組元素存放的就是一個鏈表。
? ? 1.3?ConcurrentHashMap的存取過程簡單解析 put(K key, V value):在存儲鍵值對數據時,會對鍵值進行第一次哈希運算,定位到Segment數組中的位置,確認后位置后對該位置下的所有元素進行加鎖。加鎖后再進行插入操作,對該對鍵值進行第二次哈希運算,用于確認存儲在HashEntry數組中的下標位置,并進行判斷是否需要對HashEntry數組進行擴容并且重計算HashEntry數組中所有元素的存儲。確定在HashEntry數組中的位置后,就將該鍵值對添加到鏈表后。Segment的擴容判斷比HashMap更恰當,因為HashMap是在插入元素后判斷元素是否已經到達容量的,如果到達了就進行擴容,但是很有可能擴容之后沒有新元素插入,這時HashMap就進行了一次無效的擴容。 get(Object key):先經過一次再哈希,然后使用這個哈希值通過哈希運算定位到Segment數組中的元素,再通過哈希算法定位到HashEntry數組中的元素即可。get的高效之處在于它不需要加鎖,除非讀到的值是空才會加鎖重讀。而道HashTable容器的get方法是加鎖的,所以才導致其效率極低。而ConcurrentHashMap是如何實現不加鎖的?觀察其源碼可以發現,get方法里所有使用的共享變量全部設置為volatile類型,如用于統計當前 Segement大小的count字段和用于存儲值的HashEntry的value,所有的鍵值對元素等。定義成volatile的變量,能夠在線 程之間保持可見性,能夠被多線程同時讀,并且保證不會讀到過期的值,但是只能被單線程寫 (有一種情況可以被多線程寫,就是寫入的值不依賴于原值),在get操作里只需要讀不需要寫 共享變量count和value,所以可以不用加鎖。之所以不會讀到過期的值,是因為根據Java內存模 型的happen before原則,對volatile字段的寫入操作先于讀操作,即使兩個線程同時修改和獲取 volatile變量,get操作也能拿到最新的值,這是用volatile替換鎖的經典應用場景。 size():如果要統計整個ConcurrentHashMap里元素的大小,就必須統計所有Segment里元素的大小 后求和。Segment里的全局變量count是一個volatile變量,那么在多線程場景下,是不是直接把 所有Segment的count相加就可以得到整個ConcurrentHashMap大小了呢?實際上并不是,雖然可以獲取每個Segment的count的最新值,但是如果在將這些值相加時(原子性操作以及其可見性),count的值改變了那么就會得到不準確的值。而把所有Segment的put、remove和clean方法 全部鎖住又會非常低效。所以使用modCount 變量,在put、remove和clean方法里操作元素前都會將變量modCount進行加1,那么在統計size 前后比較modCount是否發生變化,從而得知容器的大小是否發生變化。 2. ConcurrentLinkedQueue的實現原理與使用 ????ConcurrentLinkedQueue是一個基于鏈接節點的無界線程安全隊列,它采用先進先出的規則對節點進行排序,當我們添加一個元素的時候,它會添加到隊列的尾部,當我們獲取一個元素時,它會返回隊列頭部的元素。基于CAS的“wait-free”(常規無等待)來實現,CAS并不是一個算法,它是一個CPU直接支持的硬件指令,這也就在一定程度上決定了它的平臺相關性。
????當前常用的多線程同步機制可以分為下面三種類型:(在后面深入底層機制學習時在進行詳解)
volatile 變量:輕量級多線程同步機制,不會引起上下文切換和線程調度。僅提供內存可見性保證,不提供原子性。 CAS 原子指令:輕量級多線程同步機制,不會引起上下文切換和線程調度。它同時提供內存可見性和原子化更新保證。 互斥鎖:重量級多線程同步機制,可能會引起上下文切換和線程調度,它同時提供內存可見性和原子性。 ????ConcurrentLinkedQueue 的非阻塞算法實現主要可概括為下面幾點:
使用 CAS 原子指令來處理對數據的并發訪問,這是非阻塞算法得以實現的基礎。 head/tail 并非總是指向隊列的頭 / 尾節點,也就是說允許隊列處于不一致狀態。 這個特性把入隊 /出隊時,原本需要一起原子化執行的兩個步驟分離開來,從而縮小了入隊 /出隊時需要原子化更新值的范圍到唯一變量。這是非阻塞算法得以實現的關鍵。 以批處理方式來更新head/tail,從整體上減少入隊 / 出隊操作的開銷。 ? ? 2.1 數據結構 ? ? 其數據結構與普通的隊列沒有差別,當單線程進行插入或刪除時與普通隊列相同,但是如果對于普通隊列進行多線程的刪除插入操作時,就會出現插隊現象,而ConcurrentLinkedQueue則避免了這種現象的發生。
? ? 2.2 入隊列實現 ????整個入隊過程主要做兩件事情:第一是定位出尾節點;第二是使用 CAS將入隊節點設置成尾節點的next節點,如不成功則重試。
//實現源碼
public boolean offer(E e) {checkNotNull(e);final Node<E> newNode = new Node<E>(e);for (Node<E> t = tail, p = t;;) {Node<E> q = p.next;if (q == null) {// p is last nodeif (p.casNext(null, newNode)) {// Successful CAS is the linearization point// for e to become an element of this queue,// and for newNode to become "live".if (p != t) // hop two nodes at a timecasTail(t, newNode); // Failure is OK.return true;}// Lost CAS race to another thread; re-read next}else if (p == q)// We have fallen off list. If tail is unchanged, it// will also be off-list, in which case we need to// jump to head, from which all live nodes are always// reachable. Else the new tail is a better bet.p = (t != (t = tail)) ? t : head;else// Check for tail updates after two hops.p = (p != t && t != (t = tail)) ? t : q;}}
定位尾節點:查看源碼可以發現隊列中擁head表示頭結點,tail表示尾節點,但是在多線程入隊列時tail節點并不一定是尾節點,尾節點可能會是tail的next節點。代碼中循環體中的第一個if就是判斷tail是否有next節點,有則表示next節點可能是尾節點。獲取tail節點的next節點需要注意的是p節點等于p的next節點的情況,只有一種可能就是p節點和p的next節點都等于空,表示這個隊列剛初始化,正準備添加節點,所以需要返回head節點。 設置入隊節點為尾節點:p.casNext(null,n)方法用于將入隊節點設置為當前隊列尾節點next節點,如果p是null, 表示p是當前隊列的尾節點,如果不為null,表示有其他線程更新了尾節點,則需要重新獲取當 前隊列的尾節點。 ? ? 2.3 出隊列實現 首先獲取頭節點的元素,然后判斷頭節點元素是否為空,如果為空,表示另外一個線程已經進行了一次出隊操作將該節點的元素取走,如果不為空,則使用CAS的方式將頭節點的引用設置成null,如果CAS成功,則直接返回頭節點的元素,如果不成功,表示另外一個線程已經進行了一次出隊操作更新了head節點,導致元素發生了變化,需要重新獲取頭節點。
轉載于:https://my.oschina.net/ProgramerLife/blog/1807749
總結
以上是生活随笔 為你收集整理的并发容器与框架——并发容器(一) 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。