該文已加入開源項目:JavaGuide(一份涵蓋大部分Java程序員所須要掌握的核心知識的文檔類項目,Star 數接近 16k)。地址:github.com/Snailclimb/….java
實戰Java高併發程序設計》爲咱們總結了下面幾種你們可能會在高併發程序設計中常常遇到和使用的 JDK 爲咱們提供的併發容器。先帶你們概覽一下,下面會一一介紹到。git
JDK提供的這些容器大部分在 java.util.concurrent
包中。程序員
咱們知道 HashMap 不是線程安全的,在併發場景下若是要保證一種可行的方式是使用 Collections.synchronizedMap()
方法來包裝咱們的 HashMap。但這是經過使用一個全局的鎖來同步不一樣線程間的併發訪問,所以會帶來不可忽視的性能問題。github
因此就有了 HashMap 的線程安全版本—— ConcurrentHashMap 的誕生。在ConcurrentHashMap中,不管是讀操做仍是寫操做都能保證很高的性能:在進行讀操做時(幾乎)不須要加鎖,而在寫操做時經過鎖分段技術只對所操做的段加鎖而不影響客戶端對其它段的訪問。面試
關於 ConcurrentHashMap 相關問題,我在 《這幾道Java集合框架面試題幾乎必問》 這篇文章中已經提到過。下面梳理一下關於 ConcurrentHashMap 比較重要的問題:算法
public class CopyOnWriteArrayList<E> extends Object implements List<E>, RandomAccess, Cloneable, Serializable 複製代碼
在不少應用場景中,讀操做可能會遠遠大於寫操做。因爲讀操做根本不會修改原有的數據,所以對於每次讀取都進行加鎖實際上是一種資源浪費。咱們應該容許多個線程同時訪問List的內部數據,畢竟讀取操做是安全的。數組
這和咱們以前在多線程章節講過 ReentrantReadWriteLock
讀寫鎖的思想很是相似,也就是讀讀共享、寫寫互斥、讀寫互斥、寫讀互斥。JDK中提供了 CopyOnWriteArravList
類比相比於在讀寫鎖的思想又更進一步。爲了將讀取的性能發揮到極致,CopyOnWriteArravList
讀取是徹底不用加鎖的,而且更厲害的是:寫入也不會阻塞讀取操做。只有寫入和寫入之間須要進行同步等待。這樣一來,讀操做的性能就會大幅度提高。那它是怎麼作的呢?安全
CopyOnWriteArravList
類的全部可變操做(add,set等等)都是經過建立底層數組的新副原本實現的。當 List 須要被修改的時候,我並不修改原有內容,而是對原有數據進行一次複製,將修改的內容寫入副本。寫完以後,再將修改完的副本替換原來的數據,這樣就能夠保證寫操做不會影響讀操做了。數據結構
從 CopyOnWriteArravList
的名字就能看出CopyOnWriteArravList
是知足CopyOnWrite
的ArrayList,所謂CopyOnWrite
也就是說:在計算機,若是你想要對一塊內存進行修改時,咱們不在原有內存塊中進行寫操做,而是將內存拷貝一份,在新的內存中進行寫操做,寫完以後呢,就將指向原來內存指針指向新的內存,原來的內存就能夠被回收掉了。多線程
讀取操做沒有任何同步控制和鎖操做,理由就是內部數組 array 不會發生修改,只會被另一個 array 替換,所以能夠保證數據安全。
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
public E get(int index) {
return get(getArray(), index);
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
final Object[] getArray() {
return array;
}
複製代碼
CopyOnWriteArravList 寫入操做 add() 方法在添加集合的時候加了鎖,保證了同步,避免了多線程寫的時候會 copy 出多個副本出來。
/** * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return {@code true} (as specified by {@link Collection#add}) */
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();//加鎖
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);//拷貝新數組
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();//釋放鎖
}
}
複製代碼
Java提供的線程安全的 Queue 能夠分爲阻塞隊列和非阻塞隊列,其中阻塞隊列的典型例子是 BlockingQueue,非阻塞隊列的典型例子是ConcurrentLinkedQueue,在實際應用中要根據實際須要選用阻塞隊列或者非阻塞隊列。 阻塞隊列能夠經過加鎖來實現,非阻塞隊列能夠經過 CAS 操做實現。
從名字能夠看出,ConcurrentLinkedQueue
這個隊列使用鏈表做爲其數據結構.ConcurrentLinkedQueue 應該算是在高併發環境中性能最好的隊列了。它之全部能有很好的性能,是由於其內部複雜的實現。
ConcurrentLinkedQueue 內部代碼咱們就不分析了,你們知道ConcurrentLinkedQueue 主要使用 CAS 非阻塞算法來實現線程安全就行了。
ConcurrentLinkedQueue 適合在對性能要求相對較高,同時對隊列的讀寫存在多個線程同時進行的場景,即若是對隊列加鎖的成本較高則適合使用無鎖的ConcurrentLinkedQueue來替代。
上面咱們己經提到了 ConcurrentLinkedQueue 做爲高性能的非阻塞隊列。下面咱們要講到的是阻塞隊列——BlockingQueue。阻塞隊列(BlockingQueue)被普遍使用在「生產者-消費者」問題中,其緣由是BlockingQueue提供了可阻塞的插入和移除的方法。當隊列容器已滿,生產者線程會被阻塞,直到隊列未滿;當隊列容器爲空時,消費者線程會被阻塞,直至隊列非空時爲止。
BlockingQueue 是一個接口,繼承自 Queue,因此其實現類也能夠做爲 Queue 的實現來使用,而 Queue 又繼承自 Collection 接口。下面是 BlockingQueue 的相關實現類:
下面主要介紹一下:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,這三個 BlockingQueue 的實現類。
ArrayBlockingQueue 是 BlockingQueue 接口的有界隊列實現類,底層採用數組來實現。ArrayBlockingQueue一旦建立,容量不能改變。其併發控制採用可重入鎖來控制,不論是插入操做仍是讀取操做,都須要獲取到鎖才能進行操做。當隊列容量滿時,嘗試將元素放入隊列將致使操做阻塞;嘗試從一個空隊列中取一個元素也會一樣阻塞。
ArrayBlockingQueue 默認狀況下不能保證線程訪問隊列的公平性,所謂公平性是指嚴格按照線程等待的絕對時間順序,即最早等待的線程可以最早訪問到 ArrayBlockingQueue。而非公平性則是指訪問 ArrayBlockingQueue 的順序不是遵照嚴格的時間順序,有可能存在,當 ArrayBlockingQueue 能夠被訪問時,長時間阻塞的線程依然沒法訪問到 ArrayBlockingQueue。若是保證公平性,一般會下降吞吐量。若是須要得到公平性的 ArrayBlockingQueue,可採用以下代碼:
private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10,true);
複製代碼
LinkedBlockingQueue 底層基於單向鏈表實現的阻塞隊列,能夠當作無界隊列也能夠當作有界隊列來使用,一樣知足FIFO的特性,與ArrayBlockingQueue 相比起來具備更高的吞吐量,爲了防止 LinkedBlockingQueue 容量迅速增,損耗大量內存。一般在建立LinkedBlockingQueue 對象時,會指定其大小,若是未指定,容量等於Integer.MAX_VALUE。
相關構造方法:
/** *某種意義上的無界隊列 * Creates a {@code LinkedBlockingQueue} with a capacity of * {@link Integer#MAX_VALUE}. */
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
/** *有界隊列 * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity. * * @param capacity the capacity of this queue * @throws IllegalArgumentException if {@code capacity} is not greater * than zero */
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
複製代碼
PriorityBlockingQueue 是一個支持優先級的無界阻塞隊列。默認狀況下元素採用天然順序進行排序,也能夠經過自定義類實現 compareTo()
方法來指定元素排序規則,或者初始化時經過構造器參數 Comparator
來指定排序規則。
PriorityBlockingQueue 併發控制採用的是 ReentrantLock,隊列爲無界隊列(ArrayBlockingQueue 是有界隊列,LinkedBlockingQueue 也能夠經過在構造函數中傳入 capacity 指定隊列最大的容量,可是 PriorityBlockingQueue 只能指定初始的隊列大小,後面插入元素的時候,若是空間不夠的話會自動擴容)。
簡單地說,它就是 PriorityQueue 的線程安全版本。不能夠插入 null 值,同時,插入隊列的對象必須是可比較大小的(comparable),不然報 ClassCastException 異常。它的插入操做 put 方法不會 block,由於它是無界隊列(take 方法在隊列爲空的時候會阻塞)。
推薦文章:
《解讀 Java 併發隊列 BlockingQueue》
下面這部份內容參考了極客時間專欄《數據結構與算法之美》以及《實戰Java高併發程序設計》。
爲了引出ConcurrentSkipListMap,先帶着你們簡單理解一下跳錶。
對於一個單鏈表,即便鏈表是有序的,若是咱們想要在其中查找某個數據,也只能從頭至尾遍歷鏈表,這樣效率天然就會很低,跳錶就不同了。跳錶是一種能夠用來快速查找的數據結構,有點相似於平衡樹。它們均可以對元素進行快速的查找。但一個重要的區別是:對平衡樹的插入和刪除每每極可能致使平衡樹進行一次全局的調整。而對跳錶的插入和刪除只須要對整個數據結構的局部進行操做便可。這樣帶來的好處是:在高併發的狀況下,你會須要一個全局鎖來保證整個平衡樹的線程安全。而對於跳錶,你只須要部分鎖便可。這樣,在高併發環境下,你就能夠擁有更好的性能。而就查詢的性能而言,跳錶的時間複雜度也是 O(logn) 因此在併發數據結構中,JDK 使用跳錶來實現一個 Map。
跳錶的本質是同時維護了多個鏈表,而且鏈表是分層的,
最低層的鏈表維護了跳錶內全部的元素,每上面一層鏈表都是下面一層的了集。
跳錶內的全部鏈表的元素都是排序的。查找時,能夠從頂級鏈表開始找。一旦發現被查找的元素大於當前鏈表中的取值,就會轉入下一層鏈表繼續找。這也就是說在查找過程當中,搜索是跳躍式的。如上圖所示,在跳錶中查找元素18。
查找18 的時候原來須要遍歷 18 次,如今只須要 7 次便可。針對鏈表長度比較大的時候,構建索引查找效率的提高就會很是明顯。
從上面很容易看出,跳錶是一種利用空間換時間的算法。
使用跳錶實現Map 和使用哈希算法實現Map的另一個不一樣之處是:哈希並不會保存元素的順序,而跳錶內全部的元素都是排序的。所以在對跳錶進行遍歷時,你會獲得一個有序的結果。因此,若是你的應用須要有序性,那麼跳錶就是你不二的選擇。JDK 中實現這一數據結構的類是ConcurrentSkipListMap。
ThoughtWorks准入職Java工程師。專一Java知識分享!開源 Java 學習指南——JavaGuide(12k+ Star)的做者。公衆號多篇文章被各大技術社區轉載。公衆號後臺回覆關鍵字「1」能夠領取一份我精選的Java資源哦!