1.4 自定義執行緒池
現在你已經對建立和使用執行緒池有了初步瞭解,包括執行緒池建立引數的認識,現在我們將目光放在物件引數上,看它們在實際使用中,能達到什麼效果,這樣可以加深我們對這些引數的理解,幫助我們在後面的使用當中更加得心應手。
1.4.1 等待佇列
執行緒池等待佇列的引數型別是BlockingQueue<Runnable>
,這是一個Java
介面,它的實現類比較多,在 java.util.concurrent.Executors 應用中,用到了兩個實現類,分別是java.util.concurrent.LinkedBlockingQueue
和java.util.concurrent.SynchronousQueue
。
1.LinkedBlockingQueue
LinkedBlockingQueue
是 Java 中的一個阻塞佇列實現,它基於連結串列資料結構實現。它的特點是:
- FIFO(先進先出)。佇列中的元素按照它們被插入的順序進行處理,即先進入佇列的元素將會被優先處理。
- 執行緒安全。
LinkedBlockingQueue
是執行緒安全的,可以在併發訪問和修改場景中,保障執行緒安全。 - 阻塞操作。
LinkedBlockingQueue
支援阻塞操作,當佇列為空時,消費執行緒可以阻塞在獲取方法,知道佇列中有新的元素可用;當佇列已滿,生產執行緒會阻塞在提交元素,直到佇列有空閒接收新元素。 - 可選容量。
LinkedBlockingQueue
可以選擇是否設定容量限制,如果不設定容量限制,則佇列容量預設java.lang.Integer#MAX_VALUE
。
2.SynchronousQueue
SynchronousQueue
是Java SDK
中一種特殊的阻塞佇列,它的最大的特點就是容量為零。SynchronousQueue
容量是零,不儲存任何元素。每一個提交元素的操作都要等待一個消費執行緒移除操作,反之也成立。
在建立java.util.concurrent.Executors#newCachedThreadPool(java.util.concurrent.ThreadFactory)
執行緒池時就是用到SynchronousQueue
。根據我們之前對執行緒池建立新執行緒的分析,當向等待佇列提交任務時,呼叫了java.util.concurrent.SynchronousQueue#offer(E)
方法時返回false
,所以會直接進入建立新的執行緒邏輯,也就是java.util.concurrent.ThreadPoolExecutor#addWorker
方法。
3.LinkedBlockingDeque
LinkedBlockingDeque
是Java SDK
提供的一個雙端佇列實現,它與 LinkedBlockingQueue 一樣基於連結串列資料結構實現,不同的是LinkedBlockingDeque
因為是雙端連結串列,所以不僅新增和刪除操作不受限於固定的位置,可以java.util.concurrent.LinkedBlockingDeque#offerFirst(E)
,也可以java.util.concurrent.LinkedBlockingDeque#offerLast(E)
,同樣刪除操作也是。另外 LinkedBlockingDeque 也實現了java.util.concurrent.BlockingQueue
介面的所有方法,預設的操作是跟LinkedBlockingQueue
一樣的,在佇列末尾新增,從佇列開頭移除。原始碼如下:
public boolean offer(E e) {
return offerLast(e);
}
public E poll() {
return pollFirst();
}
這個佇列有什麼用呢?當我們使用執行緒池處理大量非同步任務的場景中,假如我們期望其中一部分非同步任務優先執行,如果要實現這樣的功能,就需要給執行緒池配置一個雙端連結串列LinkedBlockingDeque
。演示程式碼如下:
package org.funtester.performance.books.chapter01.section4;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 雙端列表線上程池應用功能示例
*/
public class DueueDemo {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, new LinkedBlockingDeque<>(10));// 建立執行緒池,使用雙端列表
for (int i = 0; i < 4; i++) {// 提交4個任務
int index = i;// 任務索引,用於標識任務,由於lambda表示式中的變數必須是final或者等效的,所以這裡使用區域性變數
Thread thread = new Thread(() -> {// 提交任務
try {
Thread.sleep(1000);// 模擬任務執行,睡眠1秒,避免任務過快執行完畢
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + " " + System.currentTimeMillis() + " " + index + " 執行任務");// 列印任務執行資訊
});
if (i == 3) {// 第4個任務插入到佇列頭部
LinkedBlockingDeque<Runnable> queue = (LinkedBlockingDeque<Runnable>) executor.getQueue();// 獲取執行緒池佇列
queue.offerFirst(thread);// 將任務插入到佇列頭部
} else {
executor.execute(thread);// 提交任務
}
System.out.println(Thread.currentThread().getName() + " " + System.currentTimeMillis() + " " + index + " 提交任務");// 列印任務提交資訊
}
executor.shutdown();// 關閉執行緒池,不再接受新任務,但會執行完佇列中的任務,並不會立即關閉
}
}
在這個演示程式碼中,將最後一個提交的任務插入了等待佇列的頭部,理論上會在第一個任務執行完成之後執行最後一個任務。至於效果如何,我們來執行程式碼驗證,控制檯列印內容如下:
main 1713000268720 0 提交任務
main 1713000268720 1 提交任務
main 1713000268720 2 提交任務
main 1713000268720 3 提交任務
pool-1-thread-1 1713000269721 0 執行任務
pool-1-thread-1 1713000270722 3 執行任務
pool-1-thread-1 1713000271723 1 執行任務
pool-1-thread-1 1713000272724 2 執行任務
跟我們預想的一模一樣,完美地實現了我們將優先順序高的任務優先執行的設想。但是這種優先順序佇列在處理優先順序時顆粒度比較粗,如果業務上有很多優先順序級別的話,這個方案就顯得難以為繼。不過沒關係,說到優先順序,Java SDK
還提供了一個java.util.PriorityQueue
供我們使用,可很不幸它並不是java.util.concurrent.BlockingQueue
介面的實現類,但是還有一個java.util.concurrent.PriorityBlockingQueue
供我們使用。
4.PriorityBlockingQueue
PriorityBlockingQueue
是Java SDK
提供的一個執行緒安全的阻塞優先順序佇列。相比較LinkedBlockingQueue
,它新增了兩點特性:
- 優先順序支援。
PriorityBlockingQueue
可以根據元素的優先順序進行排序,保障優先的元素排在佇列的頭部。 - 無界容量。
PriorityBlockingQueue
容量不受初始容量限制,可以動態擴容。
要實現多優先順序執行緒池,無法直接使用PriorityBlockingQueue
,因為PriorityBlockingQueue
要求元素必須實現java.lang.Comparable
,或者在建立時指定一個java.util.Comparator
實現類。而執行緒池等待佇列中的物件型別都是 java.lang.Runnable,要想兼顧兩個方面,必須要做點改動。我選擇建立一個新的抽象類,實現java.util.Comparator和java.lang.Runnable
,當然只會實現 java.util.Comparator 的方法,這樣依然可以使用執行緒池的提交方法java.util.concurrent.ThreadPoolExecutor#execute
和Lambda
語法。抽象類程式碼如下:
package org.funtester.performance.books.chapter01.section4;
/**
* 優先順序任務抽象類
*/
public abstract class PriorityRunnable implements Comparable<PriorityRunnable>, Runnable {
int priorityLevel;// 優先等級,值越小優先順序越高,用於優先順序佇列排序
public PriorityRunnable(int priorityLevel) {≈
this.priorityLevel = priorityLevel;
}
/**
* 用與比較兩個物件的優先順序
*
* @param o
* @return
*/
@Override
public int compareTo(PriorityRunnable o) {
return this.priorityLevel - o.priorityLevel;
}
}
下面演示多優先順序執行緒池的使用:
package org.funtester.performance.books.chapter01.section4;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 多優先順序執行緒池使用示例
*/
public class PriorityTaskDemo {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(0, 2, 60L, TimeUnit.SECONDS, new PriorityBlockingQueue<Runnable>());// 建立執行緒池,核心執行緒數0,最大執行緒數2,執行緒空閒時間60秒,任務佇列為優先順序阻塞佇列
for (int i = 0; i < 5; i++) {// 提交5個任務
int priorityLevel = 5 - i;// 優先順序遞增
executor.execute(new PriorityRunnable(priorityLevel) {// 提交任務,優先順序遞增
@Override
public void run() {
try {
Thread.sleep(1000);// 休眠1秒,模擬任務執行時間
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + " " + System.currentTimeMillis() + " " + priorityLevel + " 執行任務");// 列印任務執行資訊
}
});
System.out.println(Thread.currentThread().getName() + " " + System.currentTimeMillis() + " " + priorityLevel + " 提交任務");// 列印任務提交資訊
}
executor.shutdown();// 關閉執行緒池
}
}
控制檯列印資訊如下:
main 1713004306180 5 提交任務
main 1713004306180 4 提交任務
main 1713004306180 3 提交任務
main 1713004306180 2 提交任務
main 1713004306180 1 提交任務
pool-1-thread-1 1713004307181 5 執行任務
pool-1-thread-1 1713004308182 1 執行任務
pool-1-thread-1 1713004309183 2 執行任務
pool-1-thread-1 1713004310184 3 執行任務
pool-1-thread-1 1713004311185 4 執行任務
可以看出,除了第一個提交的優先順序為 5 的任務以外(因為這個任務提交之後直接執行,並未參加排序),其他任務均按照優先順序從高到低執行的。
至此,我們已經將 Java 執行緒池所用到還有將來各位可能用到的佇列分享完了,筆者建議初學者掌握LinkedBlockingQueue
和SynchronousQueue
即可。
FunTester 原創精華
- 混沌工程、故障測試、Web 前端
- 服務端功能測試
- 效能測試專題
- Java、Groovy、Go
- 白盒、工具、爬蟲、UI 自動化
- 理論、感悟、影片