Kotlin進階-7-阻塞隊列+線程池

目錄

1、阻塞隊列

1.1、常見阻塞場景

2、Java中的阻塞隊列

2.1、ArrayBlockingQueue

2.2、LinkedBlockingQueue

2.3、PriorityBlockingQueue

2.4、DelayQueue

2.5、SynchronousQueue

2.6、LinkedTransferQueue

2.7、LinkedBlockingDeque

3、阻塞隊列源碼解析

4、阻塞隊列使用場景

5、線程池

5.1、線程池的優勢

5.2、ThreadPoolExecutor

5.3、線程池的執行流程和原理

5.4、線程池爲什麼要使用阻塞隊列而不使用非阻塞隊列?

6、線程池的種類

6.1、FixedThreadPool

6.2、CachedThreadPool

6.3、SingleThreadExecutor

6.4、ScheduledThreadPool


1、阻塞隊列

阻塞隊列常用於生產者和消費者的場景,生產者是往隊列中添加元素的線程,消費者是從隊列中拿元素的線程。

阻塞隊列就是生產者存放元素的容器,而消費者也只從容器中拿元素。

1.1、常見阻塞場景

阻塞隊列和普通隊列不同地方在於,它會阻塞線程,而常見的阻塞場景有如下兩種:

1、當隊列中沒有數據的情況下,消費者端的所有線程都會被阻塞(掛起),直到有數據放入到隊列中。

2、當隊列中填滿數據的情況下,生產者端的所有線程都會被阻塞(掛起),直到隊列中有空的位置,這些線程會被自動喚醒。

2、Java中的阻塞隊列

Java中爲我們提供了七個阻塞隊列,他們分別如下:

2.1、ArrayBlockingQueue

ArrayBlockingQueue 是一個用數組實現的有界阻塞隊列。此隊列按照先進先出(FIFO)的原則對元素進行排序。

默認情況下不保證訪問者公平的訪問隊列,所謂公平訪問隊列是指阻塞的所有生產者線程或消費者線程,當隊列可用時,可以按照阻塞的先後順序訪問隊列,即先阻塞的生產者線程,可以先往隊列裏插入元素,先阻塞的消費者線程,可以先從隊列裏獲取元素。通常情況下爲了保證公平性會降低吞吐量。

我們可以使用以下代碼創建一個公平的阻塞隊列。

    val arrayBlockingQueue=ArrayBlockingQueue<Any>(1000,true)

2.2、LinkedBlockingQueue

基於鏈表的阻塞隊列,同ArrayListBlockingQueue類似,其內部也維持着一個數據緩衝隊列(該隊列由一個鏈表構成)。

當生產者往隊列中放入一個數據時,隊列會從生產者手中獲取數據,並緩存在隊列內部,而生產者立即返回;

只有當隊列緩衝區達到最大值緩存容量時(LinkedBlockingQueue可以通過構造函數指定該值),纔會阻塞生產者隊列,直到消費者從隊列中消費掉一份數據,生產者線程會被喚醒,反之對於消費者這端的處理也基於同樣的原理。

而LinkedBlockingQueue之所以能夠高效的處理併發數據,還因爲其對於生產者端和消費者端分別採用了獨立的鎖來控制數據同步,這也意味着在高併發的情況下生產者和消費者可以並行地操作隊列中的數據,以此來提高整個隊列的併發性能。

作爲開發者,我們需要注意的是,如果構造一個LinkedBlockingQueue對象,而沒有指定其容量大小,LinkedBlockingQueue會默認一個類似無限大小的容量(Integer.MAX_VALUE),這樣的話,如果生產者的速度一旦大於消費者的速度,也許還沒有等到隊列滿阻塞產生,系統內存就有可能已被消耗殆盡了。

2.3、PriorityBlockingQueue

PriorityBlockingQueue是一個支持優先級的無界阻塞隊列。默認情況下元素採取自然順序升序排列。繼承Comparable類實現compareTo()方法來指定元素排序規則,或者初始化PriorityBlockingQueue時,指定構造參數Comparator來對元素進行排序。需要注意的是不能保證同優先級元素的順序。

看如下示例:輸出結果會按照compareTo的比較順序進行輸出。

2.4、DelayQueue

DelayQueue是一個支持延時獲取元素的無界阻塞隊列。隊列使用PriorityQueue來實現。隊列中的元素必須實現Delayed接口,在創建元素時可以指定多久才能從隊列中獲取當前元素。只有在延遲期滿時才能從隊列中提取元素。

2.5、SynchronousQueue

SynchronousQueue是一個不存儲元素的阻塞隊列。每一個put操作必須等待一個take操作,否則不能繼續添加元素。它支持公平訪問隊列。默認情況下線程採用非公平性策略訪問隊列。

使用以下構造方法可以創建公平性訪問的SynchronousQueue,如果設置爲true,則等待的線程會採用先進先出的順序訪問隊列。

    val queue: SynchronousQueue<Any> = SynchronousQueue<Any>(true)

2.6、LinkedTransferQueue

LinkedTransferQueue是一個由鏈表結構組成的無界阻塞TransferQueue隊列。相對於其他阻塞隊列,LinkedTransferQueue多了tryTransfer和transfer方法。

(1)transfer方法

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

(2)tryTransfer方法

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

對於帶有時間限制的tryTransfer(E e,long timeout,TimeUnit unit)方法,試圖把生產者傳入的元素直接傳給消費者,但是如果沒有消費者消費該元素則等待指定的時間再返回,如果超時還沒消費元素,則返回false,如果在超時時間內消費了元素,則返回true。

2.7、LinkedBlockingDeque

LinkedBlockingDeque是一個由鏈表結構組成的雙向阻塞隊列。

所謂雙向隊列指的是可以從隊列的兩端插入和移出元素。

雙向隊列因爲多了一個操作隊列的入口,在多線程同時入隊時,也就減少了一半的競爭。

相比其他的阻塞隊列,LinkedBlockingDeque多了addFirst、addLast、offerFirst、offerLast、peekFirst和peekLast等方法,以First單詞結尾的方法,表示插入、獲取(peek)或移除雙端隊列的第一個元素。以Last單詞結尾的方法,表示插入、獲取或移除雙端隊列的最後一個元素。

另外,插入方法add等同於addLast,移除方法remove等效於removeFirst。但是take方法卻等同於takeFirst,不知道是不是JDK的bug,使用時還是用帶有First和Last後綴的方法更清楚。

在初始化LinkedBlockingDeque時可以設置容量防止其過度膨脹。

另外,雙向阻塞隊列可以運用在“工作竊取”模式中。

3、阻塞隊列源碼解析

這裏我們看一下ArrayBlockingQueue源碼:

items:ArrayBlockingQueue維護着一個Object類型的數組;

takeIndex和putIndex:表示對頭元素和隊尾元素的下標,也就是被取出和插入那端下標;

count:表示數組中存儲元素的數量;

lock:是一個可重入鎖;詳細瞭解可以看一下上一節:Kotlin進階-6-重入鎖+synchronized+volatile

notEmpty和notFull:是等待條件。notEmpty是控制消費者線程的等待條件,notFull是控制生產者線程的等待條件。

接下來我們看一下put()方法--------------------------------------------------->

從put()方法的實現我們可以看出,它首先拿到了鎖,並且獲取的是可中斷鎖,然後判斷當前數據存儲的數量是否達到了數組的長度,如果達到了,則調用notFull.await()阻塞當前線程,等待元素數量減少並且被其他線程通過調用signal()來喚醒。

看下面代碼,當我們從滿的隊列中取出了一個元素之後,他就會通過signal()方法來喚醒前面阻塞的生產者線程中的其中一個,來往隊列中添加元素。

如果數組沒滿,就會通過----------------------------->enqueue()   方法插入元素。

enqueue()的實現很簡單,就是往items數組中添加了一個新的元素,同時它會喚醒一個消費者線程來消費該元素。

我們再來看看怎麼從隊列中取元素 -----------------------------> take()

take()方法的實現和put()方法類似,都是先獲取到鎖,然後如果當前數組中沒有一個元素的話,就讓消費者線程進行等待,這時候就需要添加元素的enqueue()方法來喚醒這些等待的消費者線程。

如果有元素的話,就去enqueue()方法中去獲取該元素,同時喚醒生產者線程去添加元素。

4、阻塞隊列使用場景

除了線程池使用了阻塞隊列之外,生產者消費者模式也常常會使用到阻塞隊列。

看如下代碼:你會發現利用阻塞隊列實現的生產者和消費者模式會很簡潔。而且我們不需要考慮同步和線程間的通信問題。

5、線程池

在編程中經常使用線程來異步處理任務,比如網絡請求,或者下載圖片等,但是每個線程的創建和銷燬都需要一定的開銷。

如果每次執行一個異步任務都開啓一個新的線程的話,那這些線程的創建和銷燬將帶來很大的資源消耗,而且每個線程執行過程中你都沒控制,這個時候就需要線程池的出現,來管理這些單個線程了。

5.1、線程池的優勢

  1. 降低系統資源消耗,通過重用已存在的線程,降低線程創建和銷燬造成的消耗;
  2. 提高系統響應速度,當有任務到達時,通過複用已存在的線程,無需等待新線程的創建便能立即執行;
  3. 方便線程併發數的管控。因爲線程若是無限制的創建,可能會導致內存佔用過多而產生OOM,並且會造成cpu過度切換(cpu切換線程是有時間成本的(需要保持當前執行線程的現場,並恢復要執行線程的現場))。
  4. 提供更強大的功能,延時定時線程池。

5.2、ThreadPoolExecutor

在Executor框架中最核心的成員就是ThreadPoolExecutor,它是線程池的核心實現類,它的主要參數如下:

corePoolSize核心線程數):當默認情況下,線程池是空的,當向線程池提交一個任務時,若線程池中已創建的線程數小於corePoolSize,即便此時存在空閒線程,也會通過創建一個新線程來執行該任務,直到已創建的線程數等於corePoolSize時,(除了利用提交新任務來創建和啓動線程(按需構造),也可以通過 prestartCoreThread() 或 prestartAllCoreThreads() 方法來提前啓動線程池中的所有核心線程。)

maximumPoolSize線程池允許創建的線程數最大大小):線程池所允許的最大線程個數。其中包括核心線程和非核心線程,當隊列滿了,且已創建的線程數小於maximumPoolSize,則線程池會創建新的線程來執行任務。另外,對於無界隊列,可忽略該參數。

keepAliveTime非核心線程閒置的超時時間):非核心線程的空閒時間如果超過線程存活時間,那麼這個線程就會被銷燬。如果設置allowCoreThreadTimeOut=true,那麼這個超時時間也會應用到核心線程上

unitkeepAliveTime非核參數的時間單位,可以是 天、小時、分鐘、秒、毫秒等。

workQueue任務隊列):如果當前消費任務的核心線程全部啓動起來,這時候就會把任務放入到該隊列中,該隊列是BlockingQueue類型的,也就是阻塞隊列。

threadFactory線程工廠):可以用線程工廠爲每個創建出來的線程設置名字,一般情況無需設置該參數。

RejectedExecutorHandler(線程飽和策略):當線程池和任務隊列都滿了,再提交任務就會執行該策略,默認情況下是AbordPolicy,表示無法處理該情況,並拋出RejectedExecutorException異常。

另外還有:CallerRunsPolicy:用調用者所在的線程來處理多餘的任務。此策略提供簡單的反饋控制機制,能夠減緩新任務的提交速度。

DiscardPolicy:不能指向多餘的任務,並將該任務刪除;

DiscardOldestPolicy:丟棄最近添加到隊列的任務,並執行當前的任務。

5.3、線程池的執行流程和原理

5.4、線程池爲什麼要使用阻塞隊列而不使用非阻塞隊列?

阻塞隊列可以保證任務隊列中沒有任務時阻塞獲取任務的線程,使得線程進入wait狀態,釋放cpu資源。
當隊列中有任務時才喚醒對應線程從隊列中取出消息進行執行。使得在線程不至於一直佔用cpu資源。

6、線程池的種類

通過直接或者間接地配置ThreadPoolExecutor的參數可以創建出不同類型的線程池;其中比較常見的有4種:

6.1、FixedThreadPool

FixedThreadPool是可重用固定線程數的線程池。它的創建如下:

corePoolSizemaximumPoolSize都設置爲了相同的數量,(核心線程數==最大的線程數量)這就說明了,該線程池只有核心線程數,keepAliveTime設置爲0L,表示如果創建了非核心線程會被立即銷燬。另外任務隊列採用了無界的阻塞隊列LinkedBlockingQueue

它的執行流程如下:沒有非核心線程的步驟。

6.2、CachedThreadPool

CachedThreadPool是根據任務數量創建線程的線程池;它的實現如下:

看上面的參數:corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE都這說明該線程池,沒有核心線程數。而且這裏使用了不存儲元素的阻塞隊列SynchronouesQueue,這就說明我們往該線程池添加的所有任務,都只會交給非核心線程來處理,而且如果同時添加的數量過大,我們創建的線程數量也會很大。

它比較適合處理大量需要立即處理並且耗時很少的任務。

6.3、SingleThreadExecutor

SingleThreadExecutor是使用單個線程的線程池:

corePoolSizemaximumPoolSize都設置爲1,那就是該線程池只會有一個線程了。

6.4、ScheduledThreadPool

ScheduledThreadPool是一個能實現定時和週期性處理任務的線程池:

這裏可以看到,核心線程數=你設置的數量,線程池的線程總數是Integer的最大值,但是這裏使用了無界的DelayedWorkQueue阻塞隊列,說明該線程池不會創建非核心線程,超過核心線程數量的任務都會被添加到該無界隊列中。

該線程池通過執行scheduleAtFixedRatescheduleWithFixedDelay來提交任務,在該方法中會將任務包裝成ScheduledFutureTask 任務,然後添加到DelayWorkQueue中,DelayWorkQueue會將任務按照時間順序進行排序。

 

 

 

 

 

 

 

 

 

 

 

 

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