Java 經典面試題:聊一聊 JUC 下的 LinkedBlockingQueue

本文聊一下 JUC 下的 LinkedBlockingQueue 隊列,先說說 LinkedBlockingQueue 隊列的特點,然後再從源碼的角度聊一聊 LinkedBlockingQueue 的主要實現~

LinkedBlockingQueue 有以下特點:

  • LinkedBlockingQueue 是阻塞隊列,底層是單鏈表實現的~
  • 元素從隊列尾進隊,從隊列頭出隊,符合FIFO~
  • 可以使用 Collection 和 Iterator 兩個介面的所有操作,因為實現了兩者的介面~
  • LinkedBlockingQueue 隊列讀寫操作都加了鎖,但是讀寫用的是兩把不同的鎖,所以可以同時讀寫操作~

LinkedBlockingQueue 隊列繼承了 AbstractQueue 類,實現了 BlockingQueue 介面,LinkedBlockingQueue 主要有以下介面:

//將指定的元素插入到此隊列的尾部(如果立即可行且不會超過該隊列的容量)
//在成功時返回 true,如果此隊列已滿,則拋IllegalStateException。 
boolean add(E e); 

//將指定的元素插入到此隊列的尾部(如果立即可行且不會超過該隊列的容量) 
// 將指定的元素插入此隊列的尾部,如果該隊列已滿, 
//則在到達指定的等待時間之前等待可用的空間,該方法可中斷 
boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException; 

//將指定的元素插入此隊列的尾部,如果該隊列已滿,則一直等到(阻塞)。 
void put(E e) throws InterruptedException; 

//獲取並移除此隊列的頭部,如果沒有元素則等待(阻塞), 
//直到有元素將喚醒等待執行緒執行該操作 
E take() throws InterruptedException; 

//獲取並移除此隊列的頭,如果此隊列為空,則返回 null。 
E poll();
//獲取並移除此隊列的頭部,在指定的等待時間前一直等到獲取元素, //超過時間方法將結束
E poll(long timeout, TimeUnit unit) throws InterruptedException; 

//從此隊列中移除指定元素的單個實例(如果存在)。 
boolean remove(Object o); 

//獲取但不移除此隊列的頭元素,沒有則跑異常NoSuchElementException 
E element(); 

//獲取但不移除此隊列的頭;如果此隊列為空,則返回 null。 
E peek(); 

LinkedBlockingQueue 隊列的讀寫方法非常的多,但是常用的是 put()take()方法,因為它們兩是阻塞的,所以我們就從源碼的角度來聊一聊 LinkedBlockingQueue 隊列中這兩個方法的實現。

先來看看 put()方法,源碼如下:

public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    // 預先設置 c 的值為 -1,表示失敗
    int c = -1;
    Node<E> node = new Node<E>(e);
    // 獲取寫鎖
    final ReentrantLock putLock = this.putLock;
    // 獲取當前隊列的大小
    final AtomicInteger count = this.count;
    // 設置可中斷鎖
    putLock.lockInterruptibly();
    try {
        // 隊列滿了
        // 當前執行緒阻塞,等待其他執行緒的喚醒(其他執行緒 take 成功後就會喚醒此處執行緒)
        while (count.get() == capacity) {
            // 無限期等待
            notFull.await();
        }
        // 新增到隊列尾部
        enqueue(node);
        // 獲取當前的隊列數
        c = count.getAndIncrement();
        // 如果隊列未滿,嘗試喚醒一個put的等待執行緒
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        // 釋放鎖
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();
}

put()方法的源碼並不難,非常容易就看懂,put()方法的過程大概如下:

  • 1、先加鎖,保證容器的並發安全~
  • 2、隊列新增數據,將數據追加到隊列尾部~
  • 3、新增時,如果隊列滿了,當前執行緒是會被阻塞的,等待被喚醒~
  • 4、新增數據成功後,在適當時機,會喚起 put 的等待執行緒(隊列不滿時),或者 take 的等待執行緒(隊列不為空時),這樣保證隊列一旦滿足 put 或者 take 條件時,立馬就能喚起阻塞執行緒,繼續運行,保證了喚起的時機不被浪費offer 就有兩兩種,一種是直接返回 false,另一種是超過一定時間後返回 false~
  • 5、釋放鎖~

其他的新增方法,例如 offer,可以查看源碼,跟put() 方法大同小異,相差不大~

再來看看 take()方法,源碼如下:

public E take() throws InterruptedException {
    E x;
    // 默認負數
    int c = -1;
    // 當前鏈表的個數
    final AtomicInteger count = this.count;
    //獲取讀鎖
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();
    try {
        // 當隊列為空時,阻塞,等待其他執行緒喚醒 
        while (count.get() == 0) {
            notEmpty.await();
        }
        // 從隊列的頭部拿出一個元素
        x = dequeue();
        //減一操作,C比真實的隊列數據大一
        c = count.getAndDecrement();
        // c 大於 0 ,表示隊列有值,可以喚醒之前被阻塞的讀執行緒
        if (c > 1)
            notEmpty.signal();
    } finally {
        // 釋放鎖
        takeLock.unlock();
    }
    // 隊列未滿,可以喚醒 put 等待執行緒~
    if (c == capacity)
        signalNotFull();
    return x;
}

take()方法跟 put() 方法類似,是一個相反的操作,我就不做過多的說明了~

以上就是 LinkedBlockingQueue 隊列的簡單源碼解析,希望對你的面試或者工作有所幫助,感謝你的閱讀~

歡迎關注公眾號【互聯網平頭哥】,一起成長,一起進步~。

互聯網平頭哥

Tags: