BlockingQueue及其實現

原文鏈接:https://www.jianshu.com/p/7b2f1fa616c6

轉載至:https://www.jianshu.com/p/7b2f1fa616c6

  1. 前言

BlockingQueue即阻塞隊列,它是基於ReentrantLock,依據它的基本原理,我們可以實現Web中的長連接聊天功能,當然其最常用的還是用於實現生產者與消費者模式,大致如下圖所示:
在這裏插入圖片描述

    在Java中,BlockingQueue是一個接口,它的實現類有ArrayBlockingQueue、DelayQueue、 LinkedBlockingDeque、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等,它們的區別主要體現在存儲結構上或對元素操作上的不同,但是對於take與put操作的原理,卻是類似的。
  1. 阻塞與非阻塞

入隊

    offer(E e):如果隊列沒滿,立即返回true; 如果隊列滿了,立即返回false-->不阻塞
    put(E e):如果隊列滿了,一直阻塞,直到隊列不滿了或者線程被中斷-->阻塞
    offer(E e, long timeout, TimeUnit unit):在隊尾插入一個元素,,如果隊列已滿,則進入等待 -->阻塞 ,
    	直到出現以下三種情況:
			被喚醒
			等待時間超時
			當前線程被中斷

出隊

 	poll():如果沒有元素,直接返回null;如果有元素,出隊
    take():如果隊列空了,一直阻塞,直到隊列不爲空或者線程被中斷-->阻塞
    poll(long timeout, TimeUnit unit):如果隊列不空,出隊;如果隊列已空且已經超時,返回null;如果隊列已空且時間未超時,則進入等待,
    	直到出現以下三種情況:
    		被喚醒
    		等待時間超時
    		當前線程被中斷
  1. LinkedBlockingQueue 源碼分析

LinkedBlockingQueue是一個基於鏈表實現的可選容量的阻塞隊列。隊頭的元素是插入時間最長的,隊尾的元素是最新插入的。新的元素將會被插入到隊列的尾部。

LinkedBlockingQueue的容量限制是可選的,如果在初始化時沒有指定容量,那麼默認使用int的最大值作爲隊列容量。
底層數據結構

LinkedBlockingQueue內部是使用鏈表實現一個隊列的,但是卻有別於一般的隊列,在於該隊列至少有一個節點,頭節點不含有元素。結構圖如下:
在這裏插入圖片描述
原理

LinkedBlockingQueue中維持兩把鎖,一把鎖用於入隊,一把鎖用於出隊,這也就意味着,同一時刻,只能有一個線程執行入隊,其餘執行入隊的線程將會被阻塞;同時,可以有另一個線程執行出隊,其餘執行出隊的線程將會被阻塞。換句話說,雖然入隊和出隊兩個操作同時均只能有一個線程操作,但是可以一個入隊線程和一個出隊線程共同執行,也就意味着可能同時有兩個線程在操作隊列,那麼爲了維持線程安全,LinkedBlockingQueue使用一個AtomicInterger類型的變量表示當前隊列中含有的元素個數,所以可以確保兩個線程之間操作底層隊列是線程安全的。
源碼分析

LinkedBlockingQueue可以指定容量,內部維持一個隊列,所以有一個頭節點head和一個尾節點last,內部維持兩把鎖,一個用於入隊,一個用於出隊,還有鎖關聯的Condition對象。主要對象的定義如下:

	//容量,如果沒有指定,該值爲Integer.MAX_VALUE;
    private final int capacity;
    //當前隊列中的元素
    private final AtomicInteger count =new AtomicInteger();
    //隊列頭節點,始終滿足head.item==null
    transient Node head;
    //隊列的尾節點,始終滿足last.next==null
    private transient Node last;
    //用於出隊的鎖
    private final ReentrantLock takeLock =new ReentrantLock();
    //當隊列爲空時,保存執行出隊的線程
    private final Condition notEmpty = takeLock.newCondition();
    //用於入隊的鎖
    private final ReentrantLock putLock =new ReentrantLock();
    //當隊列滿時,保存執行入隊的線程
    private final Condition notFull = putLock.newCondition();

put(E e)方法
put(E e)方法用於將一個元素插入到隊列的尾部,其實現如下:

 public void put(E e)throws InterruptedException {
    //不允許元素爲null
        if (e ==null)throw new NullPointerException();
        int c = -1;
        //以當前元素新建一個節點
        Node node =new Node(e);
        final ReentrantLock putLock =this.putLock;
        final AtomicInteger count =this.count;
        //獲得入隊的鎖
        putLock.lockInterruptibly();
        try {
           //如果隊列已滿,那麼將該線程加入到Condition的等待隊列中
            while (count.get() == capacity) {
                 notFull.await();
            }
           //將節點入隊
            enqueue(node);
            //得到插入之前隊列的元素個數
            c = count.getAndIncrement();
            //如果還可以插入元素,那麼釋放等待的入隊線程
            if (c +1 < capacity){
                  notFull.signal();
            }
    	}finally {
    		//解鎖
            putLock.unlock();
        }
    	//通知出隊線程隊列非空
        if (c ==0)signalNotEmpty();
    }

3.1 具體入隊與出隊的原理圖:

圖中每一個節點前半部分表示封裝的數據x,後邊的表示指向的下一個引用。
在這裏插入圖片描述

初始化之後,初始化一個數據爲null,且head和last節點都是這個節點。

3.2、入隊兩個元素過後
在這裏插入圖片描述
3.3、出隊一個元素後

在這裏插入圖片描述
put方法總結:

  1. LinkedBlockingQueue不允許元素爲null。

  2. 同一時刻,只能有一個線程執行入隊操作,因爲putLock在將元素插入到隊列尾部時加鎖了

  3. 如果隊列滿了,那麼將會調用notFull的await()方法將該線程加入到Condition等待隊列中。await()方法就會釋放線程佔有的鎖,這將導致之前由於被鎖阻塞的入隊線程將會獲取到鎖,執行到while循環處,不過可能因爲由於隊列仍舊是滿的,也被加入到條件隊列中。

  4. 一旦一個出隊線程取走了一個元素,並通知了入隊等待隊列中可以釋放線程了,那麼第一個加入到Condition隊列中的將會被釋放,那麼該線程將會重新獲得put鎖,繼而執行enqueue()方法,將節點插入到隊列的尾部

  5. 然後得到插入一個節點之前的元素個數,如果隊列中還有空間可以插入,那麼就通知notFull條件的等待隊列中的線程。

  6. 通知出隊線程隊列爲空了,因爲插入一個元素之前的個數爲0,而插入一個之後隊列中的元素就從無變成了有,就可以通知因隊列爲空而阻塞的出隊線程了。

E take()方法
take()方法用於得到隊頭的元素,在隊列爲空時會阻塞,知道隊列中有元素可取。其實現如下:

public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    //獲取takeLock鎖       
     takeLock.lockInterruptibly();
    try {
        //如果隊列爲空,那麼加入到notEmpty條件的等待隊列中           
        while (count.get() == 0) {
            notEmpty.await();
        }
        //得到隊頭元素           
        x = dequeue();
        //得到取走一個元素之前隊列的元素個數           
        c = count.getAndDecrement();
        //如果隊列中還有數據可取,釋放notEmpty條件等待隊列中的第一個線程           
        if (c > 1)notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
    //如果隊列中的元素從滿到非滿,通知put線程       
   if (c == capacity)signalNotFull();
    return x;
}

take方法總結:

當隊列爲空時,就加入到notEmpty(的條件等待隊列中,當隊列不爲空時就取走一個元素,當取完發現還有元素可取時,再通知一下自己的夥伴(等待在條件隊列中的線程);最後,如果隊列從滿到非滿,通知一下put線程。

remove()方法
remove()方法用於刪除隊列中一個元素,如果隊列中不含有該元素,那麼返回false;有的話則刪除並返回true。入隊和出隊都是隻獲取一個鎖,而remove()方法需要同時獲得兩把鎖,其實現如下:

public boolean remove(Object o) {
   	//因爲隊列不包含null元素,返回false     
    if (o == null) return false;
    //獲取兩把鎖        
    fullyLock();
    try {
        //從頭的下一個節點開始遍歷           
        for (Node trail = head, p = trail.next;
            p != null;trail = p, p = p.next) {
            //如果匹配,那麼將節點從隊列中移除,trail表示前驅節點               
           if (o.equals(p.item)) {
                unlink(p, trail);
                return true;
            }
        }
        return false;
    } finally {
        //釋放兩把鎖         
        fullyUnlock();
    }
}

void fullyLock() {
   	putLock.lock();
   	takeLock.lock();
}

提問:爲什麼remove()方法同時需要兩把鎖?
LinkedBlockingQueue總結:

LinkedBlockingQueue是允許兩個線程同時在兩端進行入隊或出隊的操作的,但一端同時只能有一個線程進行操作,這是通過兩把鎖來區分的;

爲了維持底部數據的統一,引入了AtomicInteger的一個count變量,表示隊列中元素的個數。count只能在兩個地方變化,一個是入隊的方法(可以+1),另一個是出隊的方法(可以-1),而AtomicInteger是原子安全的,所以也就確保了底層隊列的數據同步。
4. ArrayBlockingQueue源碼分析

ArrayBlockingQueue底層是使用一個數組實現隊列的,並且在構造ArrayBlockingQueue時需要指定容量,也就意味着底層數組一旦創建了,容量就不能改變了,因此ArrayBlockingQueue是一個容量限制的阻塞隊列。因此,在隊列全滿時執行入隊將會阻塞,在隊列爲空時出隊同樣將會阻塞。

ArrayBlockingQueue的重要字段有如下幾個:

            /** The queued items */ 

              final Object[] items;

          /** Main lock guarding all access */ 

           final ReentrantLock lock;

        /** Condition for waiting takes */    

          private final Condition notEmpty;

        /** Condition for waiting puts */   

          private final Condition notFull;

put(E e)方法
put(E e)方法在隊列不滿的情況下,將會將元素添加到隊列尾部,如果隊列已滿,將會阻塞,直到隊列中有剩餘空間可以插入。該方法的實現如下:

public void put(E e) throws InterruptedException {
    //檢查元素是否爲null,如果是,拋出NullPointerException       
   	checkNotNull(e);
    final ReentrantLock lock = this.lock;
    //加鎖       
    lock.lockInterruptibly();
    try {
        //如果隊列已滿,阻塞,等待隊列成爲不滿狀態           
        while (count == items.length)
            notFull.await();
        //將元素入隊           
        enqueue(e);
    } finally {
        lock.unlock();
    }
}

put方法總結:

  1. ArrayBlockingQueue不允許元素爲null

  2. ArrayBlockingQueue在隊列已滿時將會調用notFull的await()方法釋放鎖並處於阻塞狀態

  3. 一旦ArrayBlockingQueue不爲滿的狀態,就將元素入隊

E take()方法
take()方法用於取走隊頭的元素,當隊列爲空時將會阻塞,直到隊列中有元素可取走時將會被釋放。其實現如下:

 public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    //首先加鎖       
    lock.lockInterruptibly();
    try {
        //如果隊列爲空,阻塞           
        while (count == 0)
            notEmpty.await();
        //隊列不爲空,調用dequeue()出隊           
        return dequeue();
    } finally {
        //釋放鎖           
      lock.unlock();
    }
}

take方法總結:

一旦獲得了鎖之後,如果隊列爲空,那麼將阻塞;否則調用dequeue()出隊一個元素。
ArrayBlockingQueue總結:

ArrayBlockingQueue的併發阻塞是通過ReentrantLock和Condition來實現的,ArrayBlockingQueue內部只有一把鎖,意味着同一時刻只有一個線程能進行入隊或者出隊的操作。

5 總結

在上面分析LinkedBlockingQueue的源碼之後,可以與ArrayBlockingQueue做一個比較。
ArrayBlockingQueue:

一個對象數組+一把鎖+兩個條件

入隊與出隊都用同一把鎖

在只有入隊高併發或出隊高併發的情況下,因爲操作數組,且不需要擴容,性能很高

採用了數組,必須指定大小,即容量有限
LinkedBlockingQueue:

一個單向鏈表+兩把鎖+兩個條件

兩把鎖,一把用於入隊,一把用於出隊,有效的避免了入隊與出隊時使用一把鎖帶來的競爭。

在入隊與出隊都高併發的情況下,性能比ArrayBlockingQueue高很多

採用了鏈表,最大容量爲整數最大值,可看做容量無限

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