LinkedBlockingQueue源碼解析

上一篇博客,我們介紹了ArrayBlockQueue,知道了它是基於數組實現的有界阻塞隊列,既然有基於數組實現的,那麼一定有基於鏈表實現的隊列了,沒錯,當然有,這就是我們今天的主角:LinkedBlockingQueue。ArrayBlockQueue是有界的,那麼LinkedBlockingQueue是有界還是無界的呢?我覺得可以說是有界的,也可以說是無界的,爲什麼這麼說呢?看下去你就知道了。

和上篇博客一樣,我們還是先看下LinkedBlockingQueue的基本應用,然後解析LinkedBlockingQueue的核心代碼。

LinkedBlockingQueue基本應用

    public static void main(String[] args) throws InterruptedException {
        LinkedBlockingQueue<Integer> linkedBlockingQueue = new LinkedBlockingQueue();

        linkedBlockingQueue.add(15);
        linkedBlockingQueue.add(60);
        linkedBlockingQueue.offer(50);
        linkedBlockingQueue.put(100);

        System.out.println(linkedBlockingQueue);

        System.out.println(linkedBlockingQueue.size());

        System.out.println(linkedBlockingQueue.take());
        System.out.println(linkedBlockingQueue);

        System.out.println(linkedBlockingQueue.poll());
        System.out.println(linkedBlockingQueue);

        System.out.println(linkedBlockingQueue.peek());
        System.out.println(linkedBlockingQueue);

        System.out.println(linkedBlockingQueue.remove(50));
        System.out.println(linkedBlockingQueue);
    }

運行結果:

[15, 60, 50, 100]
4
15
[60, 50, 100]
60
[50, 100]
50
[50, 100]
true
[100]

代碼比較簡單,先試着分析下:

  1. 創建了一個LinkedBlockingQueue 。
  2. 分別使用add/offer/put方法向LinkedBlockingQueue中添加元素,其中add方法執行了兩次。
  3. 打印出LinkedBlockingQueue:[15, 60, 50, 100]。
  4. 打印出LinkedBlockingQueue的size:4。
  5. 使用take方法彈出第一個元素,並打印出來:15。
  6. 打印出LinkedBlockingQueue:[60, 50, 100]。
  7. 使用poll方法彈出第一個元素,並打印出來:60。
  8. 打印出LinkedBlockingQueue:[50, 100]。
  9. 使用peek方法彈出第一個元素,並打印出來:50。
  10. 打印出LinkedBlockingQueue:[50, 10]。
  11. 使用remove方法,移除值爲50的元素,返回true。
  12. 打印出LinkedBlockingQueue:100。

代碼比較簡單,但是還是有些細節不明白:

  • 底層是如何保證線程安全性的?
  • 數據保存在哪裏,以什麼形式保存的?
  • offer/add/put都是往隊列裏面添加元素,區別是什麼?
  • poll/take/peek都是彈出隊列的元素,區別是什麼?

要解決上面的疑問,最好的途徑還是看源碼,下面我們就來看看LinkedBlockingQueue的核心源碼。

LinkedBlockingQueue源碼解析

構造方法

LinkedBlockingQueue提供了三個構造方法,如下圖所示:



我們一個一個來分析。

LinkedBlockingQueue()
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

無參的構造方法竟然直接把“鍋”甩出去了,甩給了另外一個構造方法,但是我們要注意傳的參數:Integer.MAX_VALUE。

LinkedBlockingQueue(int capacity)
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }
  1. 判斷傳入的capacity是否合法,如果不大於0,直接拋出異常。
  2. 把傳入的capacity賦值給capacity。
  3. 新建一個Node節點,並且把此節點賦值給head和last字段。

這個capacity是什麼呢?如果大家對代碼有一定的感覺的話,應該很容易猜到這是LinkedBlockingQueue的最大容量。如果我們調用無參的構造方法來創建LinkedBlockingQueue的話,那麼它的最大容量就是Integer.MAX_VALUE,我們把它稱爲“無界”,但是我們也可以指定最大容量,那麼此隊列又是一個“有界”隊列了,所以有些博客很草率的說LinkedBlockingQueue是有界隊列,或者是無界隊列,個人認爲這是不嚴謹的。

我們再來看看這個Node是個什麼鬼:

    static class Node<E> {
        E item;

        Node<E> next;

        Node(E x) { item = x; }
    }

是不是有一種莫名的親切感,很明顯,這是單向鏈表的實現呀,next指向的就是下一個Node。

LinkedBlockingQueue(Collection<? extends E> c)
    public LinkedBlockingQueue(Collection<? extends E> c) {
        this(Integer.MAX_VALUE);//調用第二個構造方法,傳入的capacity是Int的最大值,可以說 是一個無界隊列。
        final ReentrantLock putLock = this.putLock;
        putLock.lock(); //開啓排他鎖
        try {
            int n = 0;//用於記錄LinkedBlockingQueue的size
            //循環傳入的c集合
            for (E e : c) {
                if (e == null)//如果e==null,則拋出空指針異常
                    throw new NullPointerException();
                if (n == capacity)//如果n==capacity,說明到了最大的容量,則拋出“Queue full”異常
                    throw new IllegalStateException("Queue full");
                enqueue(new Node<E>(e));//入隊操作
                ++n;//n自增
            }
            count.set(n);//設置count
        } finally {
            putLock.unlock();//釋放排他鎖
        }
    }
  1. 調用第二個構造方法,傳入了int的最大值,所以可以說此時LinkedBlockingQueue是無界隊列。
  2. 開啓排他鎖putLock 。
  3. 定義了一個變量n,用來記錄當前LinkedBlockingQueue的size。
  4. 循環傳入的集合,如果其中的元素爲null,則拋出空指針異常,如果n==capacity,說明到了最大的容量,則拋出“Queue full”異常,否則執行enqueue操作來進行入隊,然後n進行自增。
  5. 設置count爲n,由此可知,count就是LinkedBlockingQueue的size了。
  6. 在finally中釋放排他鎖putLock 。

offer

    public boolean offer(E e) {
        if (e == null) throw new NullPointerException();//如果傳入的元素爲NULL,拋出異常
        final AtomicInteger count = this.count;//取出count
        if (count.get() == capacity)//如果count==capacity,說明到了最大容量,直接返回false
            return false;
        int c = -1;//表示size
        Node<E> node = new Node<E>(e);//新建Node節點
        final ReentrantLock putLock = this.putLock;
        putLock.lock();//開啓排他鎖
        try {
            if (count.get() < capacity) {//如果count<capacity,說明還沒有達到最大容量
                enqueue(node);//入隊操作
                c = count.getAndIncrement();//獲得count,賦值給c後完成自增操作
                if (c + 1 < capacity)//如果c+1 <capacity,說明還有剩餘的空間,喚醒因爲調用notFull的await方法而被阻塞的線程
                    notFull.signal();
            }
        } finally {
            putLock.unlock();//在finally中釋放排他鎖
        }
        if (c == 0)//如果c==0,說明釋放putLock的時候,隊列中有一個元素,則調用signalNotEmpty
            signalNotEmpty();
        return c >= 0;
    }
  1. 如果傳進來的元素爲null,則拋出異常。
  2. 把本類實例的count賦值給局部變量count。
  3. 如果count==capacity,說明到了最大的容量,直接返回false。
  4. 定義局部變量c,用來表示size,初始值是-1。
  5. 新建Node節點。
  6. 開啓排他鎖putLock。
  7. 如果count>=capacity,說明到了最大的容量,釋放排他鎖後,返回false,因爲此時c=-1,c>=0爲false;如果count<capacity,說明還有剩餘空間,繼續往下執行。這裏需要思考一個問題,爲什麼第三步已經判斷過了是否還有剩餘空間,這裏還要再判斷一次呢?因爲可能有多個線程都在執行add/offer/put方法,當隊列沒有滿的時候,多個線程同時執行到第三步(第三步的時候還沒有開啓排他鎖),然後同時往下走,所以開啓排他鎖後,還需要重新判斷下。
  8. 執行入隊操作。
  9. 獲得count,並且賦值給c後,完成自增的操作。注意,是先賦值後自增,賦值和自增的先後順序會直接影響到後面的判斷邏輯。
  10. 如果c+1<capacity,說明還有剩餘的空間,喚醒因爲調用notFull的await方法而被阻塞的線程。這裏爲什麼要+1再進行判斷?因爲在第9步中,是先賦值後自增,也就是說局部變量c保存的還是入隊之前LinkedBlockingQueue的size,所以要先進行+1操作,得到的纔是當前LinkedBlockingQueue的size。
  11. 在finally中,釋放排他鎖putLock。
  12. 如果c==0,說明在釋放putLock排他鎖的時候,隊列中有且只有一個元素,則調用signalNotEmpty方法。讓我們來看看signalNotEmpty方法:
    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

代碼比較簡單,就是開啓排他鎖,喚醒因爲調用notEmpty的await方法而被阻塞的線程,但是這裏需要注意,這裏獲得的排他鎖已經不再是putLock,而是takeLock。

add

    public boolean add(E e) {
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }

add方法直接調用了offer方法,但是add和offer還不完全一樣,當隊列滿了,如果調用offer方法,會直接返回false,但是調用add方法,會拋出"Queue full"的異常。

put

    public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();//如果傳入的元素爲NULL,拋出異常
        int c = -1;//表示size
        Node<E> node = new Node<E>(e);//新建Node節點
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;//獲得count
        putLock.lockInterruptibly();//開啓排他鎖
        try {
            //如果到了最大容量,調用notFull的await方法,等待喚醒,用while循環,是爲了防止虛假喚醒
            while (count.get() == capacity) {
                notFull.await();
            }
            enqueue(node);//入隊
            c = count.getAndIncrement();//count先賦值給c後,再進行自增操作
            if (c + 1 < capacity)//如果c+1<capacity,調用notFull的signal方法,喚醒因爲調用notFull的await方法而被阻塞的線程
                notFull.signal();
        } finally {
            putLock.unlock();//釋放排他鎖
        }
        if (c == 0)//如果隊列中有一個元素,喚醒因爲調用notEmpty的await方法而被阻塞的線程
            signalNotEmpty();
    }
  1. 如果傳入的元素爲NULL,則拋出異常。
  2. 定義一個局部變量c,來表示size,初始值是-1。
  3. 新建Node節點。
  4. 把本類實例中的count賦值給局部變量count。
  5. 開啓排他鎖putLock。
  6. 如果到了最大容量,則調用notFull的await方法,阻塞當前線程,等待其他線程調用notFull的signal方法來喚醒自己,這裏用while循環是爲了防止虛假喚醒。
  7. 執行入隊操作。
  8. count先賦值給c後,再進行自增操作。
  9. 如果c+1<capacity,說明還有剩餘的空間,則調用notFull的signal方法,喚醒因爲調用notFull的await方法而被阻塞的線程。
  10. 釋放排他鎖putLock。
  11. 如果隊列中有且只有一個元素,喚醒因爲調用notEmpty的await方法而被阻塞的線程。

enqueue

    private void enqueue(Node<E> node) {
        last = last.next = node;
    }

入隊操作是不是特別簡單,就是把傳入的Node節點,賦值給last節點的next字段,再賦值給last字段,從而形成一個單向鏈表。

小總結

至此offer/add/put的核心源碼已經分析完畢,我們來做一個小總結,offer/add/put都是添加元素的方法,不過他們之間還是有所區別的,當隊列滿了,調用以上三個方法會出現不同的情況:

  • offer:直接返回false。
  • add:雖然內部也調用了offer方法,但是隊列滿了,會拋出異常。
  • put:線程會阻塞住,等待喚醒。

size

    public int size() {
        return count.get();
    }

沒什麼好說的,count記錄着LinkedBlockingQueue的size,獲得後返回就是了。

take

    public E take() throws InterruptedException {
        E x;
        int c = -1;//size
        final AtomicInteger count = this.count;//獲得count
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();//開啓排他鎖
        try {
            while (count.get() == 0) {//說明目前隊列中沒有數據
                notEmpty.await();//阻塞,等待喚醒
            }
            x = dequeue();//出隊
            c = count.getAndDecrement();//先賦值,後自減
            if (c > 1)//如果size>1,說明在出隊之前,隊列中有至少兩個元素
                notEmpty.signal();//喚醒因爲調用notEmpty的await方法而被阻塞的線程
        } finally {
            takeLock.unlock();//釋放排他鎖
        }
        if (c == capacity)//如果隊列中還有一個剩餘空間
            signalNotFull();
        return x;
    }
  1. 定義局部變量c,用來表示size,初始值是-1。
  2. 把本類實例的count字段賦值給臨時變量count。
  3. 開啓響應中斷的排他鎖takeLock 。
  4. 如果count==0,說明目前隊列中沒有數據,就阻塞當前線程,等待喚醒,直到其他線程調用了notEmpty的signal方法喚醒了當前線程。用while循環是爲了防止虛假喚醒。
  5. 進行出隊操作。
  6. count先賦值給c後,在進行自減操作,這裏需要注意是先賦值,後自減。
  7. 如果c>1,也就是size>1,結合上面的先賦值,後自減,可知如果滿足條件,說明在出隊之前,隊列中至少有兩個元素,則調用notEmpty的signal方法,喚醒因爲調用notEmpty的await方法而被阻塞的線程。
  8. 釋放排他鎖takeLock 。
  9. 如果執行出隊後,隊列中有且只有一個剩餘空間,換個說法,就是執行出隊操作前,隊列是滿的,則調用signalNotFull方法。

我們再來看下signalNotFull方法:

    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }
  1. 開啓排他鎖,注意這裏的排他鎖是putLock 。
  2. 調用notFull的signal方法,喚醒因爲調用notFull的await方法而被阻塞的線程。
  3. 釋放排他鎖putLock 。

poll

    public E poll() {
        final AtomicInteger count = this.count;
        if (count.get() == 0)
            return null;
        E x = null;
        int c = -1;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            if (count.get() > 0) {
                x = dequeue();
                c = count.getAndDecrement();
                if (c > 1)
                    notEmpty.signal();
            }
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

相比take方法,最大的區別就如果隊列爲空,執行take方法會阻塞當前線程,直到被喚醒,而poll方法,直接返回null。

peek

    public E peek() {
        if (count.get() == 0)
            return null;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            Node<E> first = head.next;
            if (first == null)
                return null;
            else
                return first.item;
        } finally {
            takeLock.unlock();
        }
    }

peek方法,只是拿到頭節點的值,但是不會移除該節點。

dequeue

   private E dequeue() {
        Node<E> h = head;
        Node<E> first = h.next;
        h.next = h; // help GC
        head = first;
        E x = first.item;
        first.item = null;
        return x;
    }

沒什麼好說的,就是彈出元素,並且移除彈出的元素。

小總結

至此take/poll/peek的核心源碼已經分析完畢,我們來做一個小總結,take/poll/peek都是獲得頭節點值的方法,不過他們之間還是有所區別的:

  • take:當隊列爲空,會阻塞當前線程,直到被喚醒。會進行出隊操作,移除獲得的節點。
  • poll:當隊列爲空,直接返回null。會進行出隊操作,移除獲得的節點。
  • put:當隊列爲空,直接返回null。不會移除節點。

LinkedBlockingQueue的核心源碼分析到這裏完畢了,謝謝大家。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章