【連載 05】自定義執行緒池(上)

FunTester發表於2024-12-19

1.4 自定義執行緒池

現在你已經對建立和使用執行緒池有了初步瞭解,包括執行緒池建立引數的認識,現在我們將目光放在物件引數上,看它們在實際使用中,能達到什麼效果,這樣可以加深我們對這些引數的理解,幫助我們在後面的使用當中更加得心應手。

1.4.1 等待佇列

執行緒池等待佇列的引數型別是BlockingQueue<Runnable>,這是一個Java介面,它的實現類比較多,在 java.util.concurrent.Executors 應用中,用到了兩個實現類,分別是java.util.concurrent.LinkedBlockingQueuejava.util.concurrent.SynchronousQueue

1.LinkedBlockingQueue

LinkedBlockingQueue 是 Java 中的一個阻塞佇列實現,它基於連結串列資料結構實現。它的特點是:

  • FIFO(先進先出)。佇列中的元素按照它們被插入的順序進行處理,即先進入佇列的元素將會被優先處理。
  • 執行緒安全。LinkedBlockingQueue 是執行緒安全的,可以在併發訪問和修改場景中,保障執行緒安全。
  • 阻塞操作。LinkedBlockingQueue 支援阻塞操作,當佇列為空時,消費執行緒可以阻塞在獲取方法,知道佇列中有新的元素可用;當佇列已滿,生產執行緒會阻塞在提交元素,直到佇列有空閒接收新元素。
  • 可選容量。LinkedBlockingQueue 可以選擇是否設定容量限制,如果不設定容量限制,則佇列容量預設java.lang.Integer#MAX_VALUE

2.SynchronousQueue

SynchronousQueueJava 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

LinkedBlockingDequeJava 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

PriorityBlockingQueueJava 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#executeLambda語法。抽象類程式碼如下:

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 執行緒池所用到還有將來各位可能用到的佇列分享完了,筆者建議初學者掌握LinkedBlockingQueueSynchronousQueue即可。

FunTester 原創精華
  • 混沌工程、故障測試、Web 前端
  • 服務端功能測試
  • 效能測試專題
  • Java、Groovy、Go
  • 白盒、工具、爬蟲、UI 自動化
  • 理論、感悟、影片
如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援
暫無回覆。

相關文章