帶著問題來閱讀
1、Java有哪些集合
2、不同集合的應用場景分別是哪些
3、哪些實現類是執行緒安全的
4、為什麼Java集合不能存放基本型別
5、集合的fail-fast和fail-safe是什麼
Java集合概覽
Java通過Java Collections Framework(JCF)為開發者提供了一系列集合介面和實現,所謂集合,就是多個Java物件的聚集。
學習過資料結構的同學們對各類集合的定義肯定不陌生,Java通過提供一系列的內建資料結構實現,為開發者提高了開發的便利性,提升了程式的相容性,降低了程式設計的複雜性。
圖片出自https://www.pdai.tech/md/java/collection/java-collection-all.html
Java集合包含兩個頂層介面:Collection和Map,Collection是主要儲存物件的集合,Map是儲存鍵值對的集合。
下面我們對兩類集合的實現做簡單介紹。
Collection
Collection是Jdk1.2版本引入的介面,用於描述儲存物件的集合,它的擴充套件介面有List、Queue、Set。
List
List以列表形式順序存取元素,保證元素的插入順序和儲存順序一致。
實現 | 執行緒安全 | |
---|---|---|
ArrayList | 陣列 | 否 |
LinkedList | 雙向連結串列 | 否 |
CopyOnWriteArrayList | 陣列 | 是,使用CopyOnWrite保證執行緒安全 |
Vector | 陣列 | 是,使用Synchronized保證執行緒安全 |
Stack | 陣列 | 是,繼承Vector |
Vector是Jdk1.0就引入的執行緒安全的列表實現,早於Collection介面設計,採用直接在方法上新增Synchronized來保證執行緒安全,Stack是繼承Vector實現的棧結構,由於其執行緒安全的低效性,目前在實際環境均不再推薦使用。
Queue
Queue是先進先出的結構,從隊尾加入元素,隊頭彈出元素。其子介面Deque為雙端佇列,即兩頭都可以進出。
實現 | 執行緒安全 | |
---|---|---|
ArrayDeque | 迴圈陣列 | 否 |
LinkedList | 連結串列 | 否 |
PriorityQueue | 堆 | 否 |
BlockingQueue | BlockingQueue為阻塞佇列的擴充套件介面 | 是 |
由於BlockingQueue略為複雜,更涉及到一些進階應用場景,留待後續講解。
Set
Set是不重複元素集合,元素中不包含相同元素,且通常情況不保持元素插入順序
實現 | 執行緒安全 | |
---|---|---|
HashSet | 雜湊表 | 否,基於HashMap實現 |
TreeSet | 紅黑樹 | 否,基於TreeMap實現 |
LinkedHashSet | 雜湊表+連結串列 | 否,基於LinkedHashMap實現 |
CopyOnWriteArraySet | 陣列 | 是,CopyOnWrite保證 |
ConcurrentSkipListSet | 跳錶 | 是,基於ConcurrentSkipListMap實現 |
Map
Map用於儲存<Key, Value>鍵值對。
實現 | 執行緒安全 | |
---|---|---|
HashMap | 雜湊表+紅黑樹 | 否 |
TreeMap | 紅黑樹 | 否 |
LinkedHashMap | 雜湊表+連結串列+紅黑樹 | 否 |
WeakHashMap | 雜湊表 | 否 |
ConcurrentHashMap | 雜湊表+紅黑樹 | 是,基於節點CAS和Synchronized實現 |
ConcurrentSkipListMap | 跳錶 | 是 |
為什麼Java集合不能存放基本型別
Java在1.2版本引入JCF框架,Java範型是在1.5版本引入,因此在泛型引入之前集合預設以Object作為儲存型別。
以List為例
List list = new ArrayList();
list.add(123); // 自動boxing
list.add("123");
int num = (int) list.get(0);
String str = (String) list.get(1);
顯而易見該方式存在缺陷,集合內可以放入任何以Object為基類的元素,而元素的獲取方無法確定元素的具體型別,容易出現型別轉換錯誤。在1.5版本引入範型以後,對集合介面進行規範,添加了範型引數,Java的泛型機制本質上還是將具體型別擦除為Object,因此泛型集合在初始化時,無法將引數指定為非Object派生的基本型別。
什麼是fail-fast和fail-safe
List<String> list = new ArrayList();
list.add("123");
list.add("456");
//(1) throw ConcurrentModificationException
for (String s : list) {
list.remove(s);
}
//(2) 正常移除
Iterator<String> it = list.iterator();
while (it.hasNext()) {
it.next();
it.remove();
}
//(3) throw ConcurrentModificationException
new Thread(() -> {
for (String s : list) {
System.out.println(s);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException ignore)
{
}
}
}).start();
new Thread(() -> {list.add("789");}).start();
上面這段程式碼,(1) (3) 會丟擲ConcurrentModificationException,(2)可以正常移除所有元素。首先了解一下ConcurrentModificationException,當對一個物件做出的併發修改不被允許時,將丟擲這個異常。
This exception may be thrown by methods that have detected concurrent modification of an object when such modification is not permissible.
那麼(1)是單執行緒執行,(3)併發新增元素也並未對已有元素做修改,為什麼也會觸發該異常呢。
ArrayList var1 = new ArrayList();
var1.add("123");
var1.add("456");
Iterator var2 = var1.iterator();
while(var2.hasNext()) {
String var3 = (String)var2.next();
var1.remove(var3);
}
對程式碼(1)的class檔案反編譯檢視,發現foreach實際上是通過Iterator做的迭代,迭代過程中刪除是直接呼叫list.remove。我們再進入到list.iterator方法探個究竟。
/** Returns an iterator over the elements in this list in proper sequence.
* The returned iterator is fail-fast. */
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
....
int expectedModCount = modCount;
....
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
iterator方法會建立一個Itr物件,在其創時會複製modCount到expectedModCount,每進行迭代時都會判斷兩個值是否相同,如果不同則丟擲ConcurrentModificationException。
再來看modCount是一個繼承自AbstractList的成員變數,用於記錄list被修改的次數,每當呼叫add/remove時,modCount都會加1。
// The number of times this list has been structurally modified.
protected transient int modCount = 0;
那麼問題就很明顯了,每當對list進行修改modCount都會改變,而foreach的iterator記錄的是迭代物件建立時刻的modCount值,接下來的迭代過程中,由於呼叫了list的修改方法,改變了其中modCount的值,導致modCount != expectedModCount,於是就丟擲了異常。(3)程式碼是相同的問題,不再進行贅述。
* <p><a name="fail-fast">
* The iterators returned by this class's {@link #iterator() iterator} and
* {@link #listIterator(int) listIterator} methods are <em>fail-fast</em>:</a>
* if the list is structurally modified at any time after the iterator is
* created, in any way except through the iterator's own
* {@link ListIterator#remove() remove} or
* {@link ListIterator#add(Object) add} methods, the iterator will throw a
* {@link ConcurrentModificationException}. Thus, in the face of
* concurrent modification, the iterator fails quickly and cleanly, rather
* than risking arbitrary, non-deterministic behavior at an undetermined
* time in the future.
在所有Java集合類中,直接位於java.util下除Vector、Stack、HashTable外,所有的集合都是fail-fast的,而在java.util.concurrent下的集合都是fail-safe的,即可以併發的遍歷和修改集合,具體實現由各自的執行緒安全機制保證。
為什麼需要fail-fast
fail-fast意為快速失敗,在非執行緒安全的集合應用場景中,併發對集合做的新增/刪除,可能導致另一個正在遍歷集合的執行緒出現未知的錯誤如陣列越界。因此非執行緒安全的集合實現引入fail-fast以此來快速中斷執行緒,避免引發未知的連鎖問題。