Java集合高頻面試題

程式設計師大彬發表於2021-12-12

本文目錄

  • 常見的集合有哪些?
  • List 、Set和Map 的區別
  • ArrayList 瞭解嗎?
  • ArrayList 的擴容機制?
  • 怎麼在遍歷 ArrayList 時移除一個元素?
  • Arraylist 和 Vector 的區別
  • Arraylist 與 LinkedList 區別
  • HashMap

    • 解決hash衝突的辦法有哪些?HashMap用的哪種?
    • 使用的hash演算法?
    • 擴容過程?
    • put方法流程?
    • 紅黑樹的特點?
    • 為什麼使用紅黑樹而不使用AVL樹?
    • 在解決 hash 衝突的時候,為什麼選擇先用連結串列,再轉紅黑樹?
    • HashMap 的長度為什麼是 2 的冪次方?
    • HashMap預設載入因子是多少?為什麼是 0.75?
    • 一般用什麼作為HashMap的key?
    • HashMap為什麼執行緒不安全?
    • HashMap和HashTable的區別?
  • LinkedHashMap底層原理?
  • 講一下TreeMap?
  • HashSet底層原理?
  • HashSet、LinkedHashSet 和 TreeSet 的區別?
  • 什麼是fail fast?
  • 什麼是fail safe?
  • 講一下ArrayDeque?
  • 哪些集合類是執行緒安全的?哪些不安全?
  • 迭代器 Iterator 是什麼?
  • Iterator 和 ListIterator 有什麼區別?
  • 併發容器

    • ConcurrentHashMap

      • put執行流程?
      • 怎麼擴容?
      • ConcurrentHashMap 和 Hashtable 的區別?
    • CopyOnWrite
    • ConcurrentLinkedQueue
    • 阻塞佇列

      • JDK提供的阻塞佇列
      • 原理

另外給大家分享一份精心整理的大廠高頻面試題PDF,需要的小夥伴可以自行下載:

http://mp.weixin.qq.com/s?__b...

常見的集合有哪些?

Java集合類主要由兩個介面CollectionMap派生出來的,Collection有三個子介面:List、Set、Queue。

Java集合框架圖如下:

List代表了有序可重複集合,可直接根據元素的索引來訪問;Set代表無序不可重複集合,只能根據元素本身來訪問;Queue是佇列集合。Map代表的是儲存key-value對的集合,可根據元素的key來訪問value。

集合體系中常用的實現類有ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等實現類。

List 、Set和Map 的區別

  • List 以索引來存取元素,有序的,元素是允許重複的,可以插入多個null;
  • Set 不能存放重複元素,無序的,只允許一個null;
  • Map 儲存鍵值對對映;
  • List 底層實現有陣列、連結串列兩種方式;Set、Map 容器有基於雜湊儲存和紅黑樹兩種方式實現;
  • Set 基於 Map 實現,Set 裡的元素值就是 Map的鍵值。

ArrayList 瞭解嗎?

ArrayList 的底層是動態陣列,它的容量能動態增長。在新增大量元素前,應用可以使用ensureCapacity操作增加 ArrayList 例項的容量。ArrayList 繼承了 AbstractList ,並實現了 List 介面。

ArrayList 的擴容機制?

ArrayList擴容的本質就是計算出新的擴容陣列的size後例項化,並將原有陣列內容複製到新陣列中去。預設情況下,新的容量會是原容量的1.5倍。以JDK1.8為例說明:

public boolean add(E e) {
    //判斷是否可以容納e,若能,則直接新增在末尾;若不能,則進行擴容,然後再把e新增在末尾
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //將e新增到陣列末尾
    elementData[size++] = e;
    return true;
    }

// 每次在add()一個元素時,arraylist都需要對這個list的容量進行一個判斷。通過ensureCapacityInternal()方法確保當前ArrayList維護的陣列具有儲存新元素的能力,經過處理之後將元素儲存在陣列elementData的尾部

private void ensureCapacityInternal(int minCapacity) {
      ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
        //如果傳入的是個空陣列則最小容量取預設容量與minCapacity之間的最大值
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
    
  private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // 若ArrayList已有的儲存能力滿足最低儲存要求,則返回add直接新增元素;如果最低要求的儲存能力>ArrayList已有的儲存能力,這就表示ArrayList的儲存能力不足,因此需要呼叫 grow();方法進行擴容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }


private void grow(int minCapacity) {
        // 獲取elementData陣列的記憶體空間長度
        int oldCapacity = elementData.length;
        // 擴容至原來的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //校驗容量是否夠
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //若預設值大於預設的最大值,檢查是否溢位
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 呼叫Arrays.copyOf方法將elementData陣列指向新的記憶體空間
         //並將elementData的資料複製到新的記憶體空間
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

怎麼在遍歷 ArrayList 時移除一個元素?

foreach刪除會導致快速失敗問題,可以使用迭代器的 remove() 方法。

Iterator itr = list.iterator();
while(itr.hasNext()) {
      if(itr.next().equals("jay") {
        itr.remove();
      }
}

Arraylist 和 Vector 的區別

  1. ArrayList在記憶體不夠時預設是擴充套件50% + 1個,Vector是預設擴充套件1倍。
  2. Vector屬於執行緒安全級別的,但是大多數情況下不使用Vector,因為操作Vector效率比較低。

Arraylist 與 LinkedList 區別

  1. ArrayList基於動態陣列實現;LinkedList基於連結串列實現。
  2. 對於隨機index訪問的get和set方法,ArrayList的速度要優於LinkedList。因為ArrayList直接通過陣列下標直接找到元素;LinkedList要移動指標遍歷每個元素直到找到為止。
  3. 新增和刪除元素,LinkedList的速度要優於ArrayList。因為ArrayList在新增和刪除元素時,可能擴容和複製陣列;LinkedList例項化物件需要時間外,只需要修改指標即可。

HashMap

HashMap 使用陣列+連結串列+紅黑樹(JDK1.8增加了紅黑樹部分)實現的, 連結串列長度大於8(TREEIFY_THRESHOLD)時,會把連結串列轉換為紅黑樹,紅黑樹節點個數小於6(UNTREEIFY_THRESHOLD)時才轉化為連結串列,防止頻繁的轉化。

解決hash衝突的辦法有哪些?HashMap用的哪種?

解決Hash衝突方法有:開放定址法、再雜湊法、鏈地址法。HashMap中採用的是 鏈地址法 。

  • 開放定址法基本思想就是,如果p=H(key)出現衝突時,則以p為基礎,再次hash,p1=H(p),如果p1再次出現衝突,則以p1為基礎,以此類推,直到找到一個不衝突的雜湊地址pi。 因此開放定址法所需要的hash表的長度要大於等於所需要存放的元素,而且因為存在再次hash,所以只能在刪除的節點上做標記,而不能真正刪除節點。
  • 再雜湊法提供多個不同的hash函式,當R1=H1(key1)發生衝突時,再計算R2=H2(key1),直到沒有衝突為止。 這樣做雖然不易產生堆集,但增加了計算的時間。
  • 鏈地址法將雜湊值相同的元素構成一個同義詞的單連結串列,並將單連結串列的頭指標存放在雜湊表的第i個單元中,查詢、插入和刪除主要在同義詞連結串列中進行。連結串列法適用於經常進行插入和刪除的情況。

使用的hash演算法?

Hash演算法:取key的hashCode值、高位運算、取模運算。

h=key.hashCode() //第一步 取hashCode值
h^(h>>>16)  //第二步 高位參與運算,減少衝突
return h&(length-1);  //第三步 取模運算

在JDK1.8的實現中,優化了高位運算的演算法,通過hashCode()的高16位異或低16位實現的:這麼做可以在陣列比較小的時候,也能保證考慮到高低位都參與到Hash的計算中,可以減少衝突,同時不會有太大的開銷。

擴容過程?

1.8擴容機制:當元素個數大於threshold時,會進行擴容,使用2倍容量的陣列代替原有陣列。採用尾插入的方式將原陣列元素拷貝到新陣列。1.8擴容之後連結串列元素相對位置沒有變化,而1.7擴容之後連結串列元素會倒置。

1.7連結串列新節點採用的是頭插法,這樣線上程一擴容遷移元素時,會將元素順序改變,導致兩個執行緒中出現元素的相互指向而形成迴圈連結串列,1.8採用了尾插法,避免了這種情況的發生。

原陣列的元素在重新計算hash之後,因為陣列容量n變為2倍,那麼n-1的mask範圍在高位多1bit。在元素拷貝過程不需要重新計算元素在陣列中的位置,只需要看看原來的hash值新增的那個bit是1還是0,是0的話索引沒變,是1的話索引變成“原索引+oldCap”(根據e.hash & (oldCap - 1) == 0判斷) 。這樣可以省去重新計算hash值的時間,而且由於新增的1bit是0還是1可以認為是隨機的,因此resize的過程會均勻的把之前的衝突的節點分散到新的bucket。

put方法流程?

  1. 如果table沒有初始化就先進行初始化過程
  2. 使用hash演算法計算key的索引
  3. 判斷索引處有沒有存在元素,沒有就直接插入
  4. 如果索引處存在元素,則遍歷插入,有兩種情況,一種是連結串列形式就直接遍歷到尾端插入,一種是紅黑樹就按照紅黑樹結構插入
  5. 連結串列的數量大於閾值8,就要轉換成紅黑樹的結構
  6. 新增成功後會檢查是否需要擴容

紅黑樹的特點?

  • 每個節點或者是黑色,或者是紅色。
  • 根節點是黑色。
  • 每個葉子節點(NIL)是黑色。
  • 如果一個節點是紅色的,則它的子節點必須是黑色的。
  • 從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。

為什麼使用紅黑樹而不使用AVL樹?

ConcurrentHashMap 在put的時候會加鎖,使用紅黑樹插入速度更快,可以減少等待鎖釋放的時間。紅黑樹是對AVL樹的優化,只要求部分平衡,用非嚴格的平衡來換取增刪節點時候旋轉次數的降低,提高了插入和刪除的效能。

在解決 hash 衝突的時候,為什麼選擇先用連結串列,再轉紅黑樹?

因為紅黑樹需要進行左旋,右旋,變色這些操作來保持平衡,而單連結串列不需要。當元素小於 8 個的時候,連結串列結構可以保證查詢效能。當元素大於 8 個的時候, 紅黑樹搜尋時間複雜度是 O(logn),而連結串列是 O(n),此時需要紅黑樹來加快查詢速度,但是插入和刪除節點的效率變慢了。如果一開始就用紅黑樹結構,元素太少,插入和刪除節點的效率又比較慢,浪費效能。

HashMap 的長度為什麼是 2 的冪次方?

Hash 值的範圍值比較大,使用之前需要先對陣列的長度取模運算,得到的餘數才是元素存放的位置也就是對應的陣列下標。這個陣列下標的計算方法是(n - 1) & hash。將HashMap的長度定為2 的冪次方,這樣就可以使用(n - 1)&hash位運算代替%取餘的操作,提高效能。

HashMap預設載入因子是多少?為什麼是 0.75?

先看下HashMap的預設建構函式:

int threshold;             // 容納鍵值對的最大值
final float loadFactor;    // 負載因子
int modCount;  
int size;  

Node[] table的初始化長度length為16,預設的loadFactor是0.75,0.75是對空間和時間效率的一個平衡選擇,根據泊松分佈,loadFactor 取0.75碰撞最小。一般不會修改,除非在時間和空間比較特殊的情況下 :

  • 如果記憶體空間很多而又對時間效率要求很高,可以降低負載因子Load factor的值 。
  • 如果記憶體空間緊張而對時間效率要求不高,可以增加負載因子loadFactor的值,這個值可以大於1。

一般用什麼作為HashMap的key?

一般用Integer、String 這種不可變類當 HashMap 當 key。String類比較常用。

  • 因為 String 是不可變的,所以在它建立的時候 hashcode 就被快取了,不需要重新計算。這就是 HashMap 中的key經常使用字串的原因。
  • 獲取物件的時候要用到 equals() 和 hashCode() 方法,而Integer、String這些類都已經重寫了 hashCode() 以及 equals() 方法,不需要自己去重寫這兩個方法。

HashMap為什麼執行緒不安全?

  • 多執行緒下擴容死迴圈。JDK1.7中的 HashMap 使用頭插法插入元素,在多執行緒的環境下,擴容的時候有可能導致環形連結串列的出現,形成死迴圈。
  • 在JDK1.8中,在多執行緒環境下,會發生資料覆蓋的情況。

HashMap和HashTable的區別?

HashMap和Hashtable都實現了Map介面。

  1. HashMap可以接受為null的key和value,key為null的鍵值對放在下標為0的頭結點的連結串列中,而Hashtable則不行。
  2. HashMap是非執行緒安全的,HashTable是執行緒安全的。Jdk1.5提供了ConcurrentHashMap,它是HashTable的替代。
  3. Hashtable很多方法是同步方法,在單執行緒環境下它比HashMap要慢。
  4. 雜湊值的使用不同,HashTable直接使用物件的hashCode。而HashMap重新計算hash值。

LinkedHashMap底層原理?

HashMap是無序的,迭代HashMap所得到元素的順序並不是它們最初放到HashMap的順序,即不能保持它們的插入順序。

LinkedHashMap繼承於HashMap,是HashMap和LinkedList的融合體,具備兩者的特性。每次put操作都會將entry插入到雙向連結串列的尾部。

linkedhashmap

講一下TreeMap?

TreeMap是一個能比較元素大小的Map集合,會對傳入的key進行了大小排序。可以使用元素的自然順序,也可以使用集合中自定義的比較器來進行排序。

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable {
}

TreeMap 的繼承結構:

TreeMap的特點:

  1. TreeMap是有序的key-value集合,通過紅黑樹實現。根據鍵的自然順序進行排序或根據提供的Comparator進行排序。
  2. TreeMap繼承了AbstractMap,實現了NavigableMap介面,支援一系列的導航方法,給定具體搜尋目標,可以返回最接近的匹配項。如floorEntry()、ceilingEntry()分別返回小於等於、大於等於給定鍵關聯的Map.Entry()物件,不存在則返回null。lowerKey()、floorKey、ceilingKey、higherKey()只返回關聯的key。

HashSet底層原理?

HashSet 基於 HashMap 實現。放入HashSet中的元素實際上由HashMap的key來儲存,而HashMap的value則儲存了一個靜態的Object物件。

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {
    static final long serialVersionUID = -5024744406713321676L;

    private transient HashMap<E,Object> map; //基於HashMap實現
    //...
}

HashSet、LinkedHashSet 和 TreeSet 的區別?

HashSetSet 介面的主要實現類 ,HashSet 的底層是 HashMap,執行緒不安全的,可以儲存 null 值;

LinkedHashSetHashSet 的子類,能夠按照新增的順序遍歷;

TreeSet 底層使用紅黑樹,能夠按照新增元素的順序進行遍歷,排序的方式可以自定義。

什麼是fail fast?

fast-fail是Java集合的一種錯誤機制。當多個執行緒對同一個集合進行操作時,就有可能會產生fast-fail事件。例如:當執行緒a正通過iterator遍歷集合時,另一個執行緒b修改了集合的內容,此時modCount(記錄集合操作過程的修改次數)會加1,不等於expectedModCount,那麼執行緒a訪問集合的時候,就會丟擲ConcurrentModificationException,產生fast-fail事件。邊遍歷邊修改集合也會產生fast-fail事件。

解決方法:

  • 使用Colletions.synchronizedList方法或在修改集合內容的地方加上synchronized。這樣的話,增刪集合內容的同步鎖會阻塞遍歷操作,影響效能。
  • 使用CopyOnWriteArrayList來替換ArrayList。在對CopyOnWriteArrayList進行修改操作的時候,會拷貝一個新的陣列,對新的陣列進行操作,操作完成後再把引用移到新的陣列。

什麼是fail safe?

採用安全失敗機制的集合容器,在遍歷時不是直接在集合內容上訪問的,而是先複製原有集合內容,在拷貝的集合上進行遍歷。java.util.concurrent包下的容器都是安全失敗,可以在多執行緒下併發使用,併發修改。

原理:由於迭代時是對原集合的拷貝進行遍歷,所以在遍歷過程中對原集合所作的修改並不能被迭代器檢測到,所以不會觸發Concurrent Modification Exception。

缺點:基於拷貝內容的優點是避免了Concurrent Modification Exception,但同樣地,迭代器並不能訪問到修改後的內容,即:迭代器遍歷的是開始遍歷那一刻拿到的集合拷貝,在遍歷期間原集合發生的修改迭代器是不知道的。

講一下ArrayDeque?

ArrayDeque實現了雙端佇列,內部使用迴圈陣列實現,預設大小為16。它的特點有:

  1. 在兩端新增、刪除元素的效率較高
  2. 根據元素內容查詢和刪除的效率比較低。
  3. 沒有索引位置的概念,不能根據索引位置進行操作。

ArrayDeque和LinkedList都實現了Deque介面,如果只需要從兩端進行操作,ArrayDeque效率更高一些。如果同時需要根據索引位置進行操作,或者經常需要在中間進行插入和刪除(LinkedList有相應的 api,如add(int index, E e)),則應該選LinkedList。

ArrayDeque和LinkedList都是執行緒不安全的,可以使用Collections工具類中synchronizedXxx()轉換成執行緒同步。

哪些集合類是執行緒安全的?哪些不安全?

線性安全的集合類:

  • Vector:比ArrayList多了同步機制。
  • Hashtable。
  • ConcurrentHashMap:是一種高效並且執行緒安全的集合。
  • Stack:棧,也是執行緒安全的,繼承於Vector。

線性不安全的集合類:

  • Hashmap
  • Arraylist
  • LinkedList
  • HashSet
  • TreeSet
  • TreeMap

迭代器 Iterator 是什麼?

Iterator模式用同一種邏輯來遍歷集合。它可以把訪問邏輯從不同型別的集合類中抽象出來,不需要了解集合內部實現便可以遍歷集合元素,統一使用 Iterator 提供的介面去遍歷。它的特點是更加安全,因為它可以保證,在當前遍歷的集合元素被更改的時候,就會丟擲 ConcurrentModificationException 異常。

public interface Collection<E> extends Iterable<E> {
    Iterator<E> iterator();
}

主要有三個方法:hasNext()、next()和remove()。

Iterator 和 ListIterator 有什麼區別?

ListIterator 是 Iterator的增強版。

  • ListIterator遍歷可以是逆向的,因為有previous()和hasPrevious()方法,而Iterator不可以。
  • ListIterator有add()方法,可以向List新增物件,而Iterator卻不能。
  • ListIterator可以定位當前的索引位置,因為有nextIndex()和previousIndex()方法,而Iterator不可以。
  • ListIterator可以實現物件的修改,set()方法可以實現。Iierator僅能遍歷,不能修改。
  • ListIterator只能用於遍歷List及其子類,Iterator可用來遍歷所有集合。

併發容器

JDK 提供的這些容器大部分在 java.util.concurrent 包中。

  • ConcurrentHashMap: 執行緒安全的 HashMap
  • CopyOnWriteArrayList: 執行緒安全的 List,在讀多寫少的場合效能非常好,遠遠好於 Vector.
  • ConcurrentLinkedQueue: 高效的併發佇列,使用連結串列實現。可以看做一個執行緒安全的 LinkedList,這是一個非阻塞佇列。
  • BlockingQueue: 阻塞佇列介面,JDK 內部通過連結串列、陣列等方式實現了這個介面。非常適合用於作為資料共享的通道。
  • ConcurrentSkipListMap: 跳錶的實現。使用跳錶的資料結構進行快速查詢。

ConcurrentHashMap

多執行緒環境下,使用Hashmap進行put操作會引起死迴圈,應該使用支援多執行緒的 ConcurrentHashMap。

JDK1.8 ConcurrentHashMap取消了segment分段鎖,而採用CAS和synchronized來保證併發安全。資料結構採用陣列+連結串列/紅黑二叉樹。synchronized只鎖定當前連結串列或紅黑二叉樹的首節點,相比1.7鎖定HashEntry陣列,鎖粒度更小,支援更高的併發量。當連結串列長度過長時,Node會轉換成TreeNode,提高查詢速度。

put執行流程?

在put的時候需要鎖住Segment,保證併發安全。呼叫get的時候不加鎖,因為node陣列成員val和指標next是用volatile修飾的,更改後的值會立刻重新整理到主存中,保證了可見性,node陣列table也用volatile修飾,保證在執行過程對其他執行緒具有可見性。

transient volatile Node<K,V>[] table;

static class Node<K,V> implements Map.Entry<K,V> {
    volatile V val;
    volatile Node<K,V> next;
}

put 操作流程:

  1. 如果table沒有初始化就先進行初始化過程
  2. 使用hash演算法計算key的位置
  3. 如果這個位置為空則直接CAS插入,如果不為空的話,則取出這個節點來
  4. 如果取出來的節點的hash值是MOVED(-1)的話,則表示當前正在對這個陣列進行擴容,複製到新的陣列,則當前執行緒也去幫助複製
  5. 如果這個節點,不為空,也不在擴容,則通過synchronized來加鎖,進行新增操作,這裡有兩種情況,一種是連結串列形式就直接遍歷到尾端插入或者覆蓋掉相同的key,一種是紅黑樹就按照紅黑樹結構插入
  6. 連結串列的數量大於閾值8,就會轉換成紅黑樹的結構或者進行擴容(table長度小於64)
  7. 新增成功後會檢查是否需要擴容

怎麼擴容?

陣列擴容transfer方法中會設定一個步長,表示一個執行緒處理的陣列長度,最小值是16。在一個步長範圍內只有一個執行緒會對其進行復制移動操作。

ConcurrentHashMap 和 Hashtable 的區別?

  1. Hashtable通過使用synchronized修飾方法的方式來實現多執行緒同步,因此,Hashtable的同步會鎖住整個陣列。在高併發的情況下,效能會非常差。ConcurrentHashMap採用了更細粒度的鎖來提高在併發情況下的效率。注:Synchronized容器(同步容器)也是通過synchronized關鍵字來實現執行緒安全,在使用的時候會對所有的資料加鎖。
  2. Hashtable預設的大小為11,當達到閾值後,每次按照下面的公式對容量進行擴充:newCapacity = oldCapacity * 2 + 1。ConcurrentHashMap預設大小是16,擴容時容量擴大為原來的2倍。

CopyOnWrite

寫時複製。當我們往容器新增元素時,不直接往容器新增,而是先將當前容器進行復制,複製出一個新的容器,然後往新的容器新增元素,新增完元素之後,再將原容器的引用指向新容器。這樣做的好處就是可以對CopyOnWrite容器進行併發的讀而不需要加鎖,因為當前容器不會被修改。

    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock(); //add方法需要加鎖
        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();
        }
    }

從JDK1.5開始Java併發包裡提供了兩個使用CopyOnWrite機制實現的併發容器,它們是CopyOnWriteArrayList和CopyOnWriteArraySet。

CopyOnWriteArrayList中add方法新增的時候是需要加鎖的,保證同步,避免了多執行緒寫的時候複製出多個副本。讀的時候不需要加鎖,如果讀的時候有其他執行緒正在向CopyOnWriteArrayList新增資料,還是可以讀到舊的資料。

缺點:

  • 記憶體佔用問題。由於CopyOnWrite的寫時複製機制,在進行寫操作的時候,記憶體裡會同時駐紮兩個物件的記憶體。
  • CopyOnWrite容器不能保證資料的實時一致性,可能讀取到舊資料。

ConcurrentLinkedQueue

非阻塞佇列。高效的併發佇列,使用連結串列實現。可以看做一個執行緒安全的 LinkedList,通過 CAS 操作實現。

如果對佇列加鎖的成本較高則適合使用無鎖的 ConcurrentLinkedQueue 來替代。適合在對效能要求相對較高,同時有多個執行緒對佇列進行讀寫的場景。

非阻塞佇列中的幾種主要方法:
add(E e) : 將元素e插入到佇列末尾,如果插入成功,則返回true;如果插入失敗(即佇列已滿),則會丟擲異常;
remove() :移除隊首元素,若移除成功,則返回true;如果移除失敗(佇列為空),則會丟擲異常;
offer(E e) :將元素e插入到佇列末尾,如果插入成功,則返回true;如果插入失敗(即佇列已滿),則返回false;
poll() :移除並獲取隊首元素,若成功,則返回隊首元素;否則返回null;
peek() :獲取隊首元素,若成功,則返回隊首元素;否則返回null

對於非阻塞佇列,一般情況下建議使用offer、poll和peek三個方法,不建議使用add和remove方法。因為使用offer、poll和peek三個方法可以通過返回值判斷操作成功與否,而使用add和remove方法卻不能達到這樣的效果。

阻塞佇列

阻塞佇列是java.util.concurrent包下重要的資料結構,BlockingQueue提供了執行緒安全的佇列訪問方式:當阻塞佇列進行插入資料時,如果佇列已滿,執行緒將會阻塞等待直到佇列非滿;從阻塞佇列取資料時,如果佇列已空,執行緒將會阻塞等待直到佇列非空。併發包下很多高階同步類的實現都是基於BlockingQueue實現的。BlockingQueue 適合用於作為資料共享的通道。

使用阻塞演算法的佇列可以用一個鎖(入隊和出隊用同一把鎖)或兩個鎖(入隊和出隊用不同的鎖)等方式來實現。

阻塞佇列和一般的佇列的區別就在於:

  1. 多執行緒支援,多個執行緒可以安全的訪問佇列
  2. 阻塞操作,當佇列為空的時候,消費執行緒會阻塞等待佇列不為空;當佇列滿了的時候,生產執行緒就會阻塞直到佇列不滿。

方法

方法\處理方式丟擲異常返回特殊值一直阻塞超時退出
插入方法add(e)offer(e)put(e)offer(e,time,unit)
移除方法remove()poll()take()poll(time,unit)
檢查方法element()peek()不可用不可用

JDK提供的阻塞佇列

JDK 7 提供了7個阻塞佇列,如下

1、ArrayBlockingQueue

有界阻塞佇列,底層採用陣列實現。ArrayBlockingQueue 一旦建立,容量不能改變。其併發控制採用可重入鎖來控制,不管是插入操作還是讀取操作,都需要獲取到鎖才能進行操作。此佇列按照先進先出(FIFO)的原則對元素進行排序。預設情況下不能保證執行緒訪問佇列的公平性,引數fair可用於設定執行緒是否公平訪問佇列。為了保證公平性,通常會降低吞吐量。

private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10,true);//fair

2、LinkedBlockingQueue

LinkedBlockingQueue是一個用單向連結串列實現的有界阻塞佇列,可以當做無界佇列也可以當做有界佇列來使用。通常在建立 LinkedBlockingQueue 物件時,會指定佇列最大的容量。此佇列的預設和最大長度為Integer.MAX_VALUE。此佇列按照先進先出的原則對元素進行排序。與 ArrayBlockingQueue 相比起來具有更高的吞吐量。

3、PriorityBlockingQueue

支援優先順序的無界阻塞佇列。預設情況下元素採取自然順序升序排列。也可以自定義類實現compareTo()方法來指定元素排序規則,或者初始化PriorityBlockingQueue時,指定構造引數Comparator來進行排序。

PriorityBlockingQueue 只能指定初始的佇列大小,後面插入元素的時候,如果空間不夠的話會自動擴容

PriorityQueue 的執行緒安全版本。不可以插入 null 值,同時,插入佇列的物件必須是可比較大小的(comparable),否則報 ClassCastException 異常。它的插入操作 put 方法不會 block,因為它是無界佇列(take 方法在佇列為空的時候會阻塞)。

4、DelayQueue

支援延時獲取元素的無界阻塞佇列。佇列使用PriorityBlockingQueue來實現。佇列中的元素必須實現Delayed介面,在建立元素時可以指定多久才能從佇列中獲取當前元素。只有在延遲期滿時才能從佇列中提取元素。

5、SynchronousQueue

不儲存元素的阻塞佇列,每一個put必須等待一個take操作,否則不能繼續新增元素。支援公平訪問佇列。

SynchronousQueue可以看成是一個傳球手,負責把生產者執行緒處理的資料直接傳遞給消費者執行緒。佇列本身不儲存任何元素,非常適合傳遞性場景。SynchronousQueue的吞吐量高於LinkedBlockingQueue和ArrayBlockingQueue。

6、LinkedTransferQueue

由連結串列結構組成的無界阻塞TransferQueue佇列。相對於其他阻塞佇列,多了tryTransfer和transfer方法。

transfer方法:如果當前有消費者正在等待接收元素(take或者待時間限制的poll方法),transfer可以把生產者傳入的元素立刻傳給消費者。如果沒有消費者等待接收元素,則將元素放在佇列的tail節點,並等到該元素被消費者消費了才返回。

tryTransfer方法:用來試探生產者傳入的元素能否直接傳給消費者。如果沒有消費者在等待,則返回false。和上述方法的區別是該方法無論消費者是否接收,方法立即返回。而transfer方法是必須等到消費者消費了才返回。

原理

JDK使用通知模式實現阻塞佇列。所謂通知模式,就是當生產者往滿的佇列裡新增元素時會阻塞生產者,當消費者消費了一個佇列中的元素後,會通知生產者當前佇列可用。

ArrayBlockingQueue使用Condition來實現:

private final Condition notEmpty;
private final Condition notFull;

public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0) // 佇列為空時,阻塞當前消費者
            notEmpty.await();
        return dequeue();
    } finally {
        lock.unlock();
    }
}

public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length)
            notFull.await();
        enqueue(e);
    } finally {
        lock.unlock();
    }
}

private void enqueue(E x) {
    final Object[] items = this.items;
    items[putIndex] = x;
    if (++putIndex == items.length)
          putIndex = 0;
     count++;
     notEmpty.signal(); // 佇列不為空時,通知消費者獲取元素
}

本文已經收錄到github倉庫,此倉庫用於分享網際網路大廠高頻面試題、Java核心知識總結,包括Java基礎、併發、MySQL、Springboot、MyBatis、Redis、RabbitMQ等等,面試必備!歡迎大家star!
github地址:https://github.com/Tyson0314/...
如果github訪問不了,可以訪問gitee倉庫。
gitee地址:https://gitee.com/tysondai/Ja...

相關文章