博客 / 詳情

返回

線程池有哪些拒絕策略?

一位有多年開發經驗的兄弟最近正在跳槽換工作,雖然同在帝都,好幾年都沒見面了,週末約着一塊小酌一下,聊到面試被問題線程池拒絕策略的問題(木有辦法,搞技術的人,聊天不超過10句,準又回到技術上^^)。今天把聊天的內容總結一下,分享給大家。

線程池的拒絕策略是指當線程池中的線程數達到其最大容量,並且隊列也滿了時,線程池如何處理新提交的任務。在Java中,ThreadPoolExecutor提供了以下四種拒絕策略:

  • AbortPolicy(默認策略):當任務無法被線程池執行時,會拋出一個RejectedExecutionException異常。
  • CallerRunsPolicy:當任務無法被線程池執行時,會直接在調用者線程中運行這個任務。如果調用者線程正在執行一個任務,則會創建一個新線程來執行被拒絕的任務。
  • DiscardPolicy:當任務無法被線程池執行時,任務將被丟棄,不拋出異常,也不執行任務。
  • DiscardOldestPolicy:當任務無法被線程池執行時,線程池會丟棄隊列中最舊的任務,然後嘗試再次提交當前任務。

下面,V哥對四種拒絕策略再從使用場景、案例代碼來詳細解釋一下,老鐵們坐穩扶好,V哥要發車了。

1. AbortPolicy

AbortPolicy是Java線程池中默認的拒絕策略。當線程池達到其最大容量,並且工作隊列也滿了,無法再接受新的任務時,使用AbortPolicy策略會直接拋出RejectedExecutionException異常。這個異常表明任務因為線程池的資源不足而被拒絕。

業務場景

假設有一個電商平台,需要處理大量的訂單處理任務。在高流量的促銷活動期間,訂單量可能會突然激增,導致線程池中的線程數和隊列容量都達到上限。如果繼續提交任務,使用AbortPolicy策略,系統會拋出異常,提示開發者或者系統管理員需要關注線程池的資源限制問題。

示例代碼

下面是一個使用AbortPolicy策略的線程池示例代碼:

import java.util.concurrent.*;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 創建一個固定大小為5的線程池
        int numberOfThreads = 5;
        ExecutorService executor = new ThreadPoolExecutor(
            numberOfThreads,
            numberOfThreads,
            0L,
            TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>()
        );

        // 提交10個任務到線程池
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            executor.submit(() -> {
                System.out.println("Task " + finalI + " is running.");
                // 模擬任務執行時間
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        // 嘗試提交第11個任務,此時線程池和隊列已滿
        try {
            executor.submit(() -> {
                System.out.println("Task 11 is running.");
            });
        } catch (RejectedExecutionException e) {
            System.out.println("RejectedExecutionException: Task 11 was rejected.");
        }

        // 關閉線程池
        executor.shutdown();
    }
}

V哥來解釋一下

在這個示例中,我們創建了一個固定大小為5的線程池,並且使用了LinkedBlockingQueue作為工作隊列。我們提交了10個任務,每個任務簡單地打印一條消息並休眠1秒。

當嘗試提交第11個任務時,由於線程池中的線程數和隊列都已滿,任務無法被執行。此時,線程池使用默認的AbortPolicy策略,拋出RejectedExecutionException異常。這個異常可以通過捕獲來處理,例如在示例中,我們通過catch塊捕獲了這個異常,並打印了一條消息。

這種策略適合於那些不能容忍任務被丟棄或延遲執行的業務場景,因為它會立即通知調用者任務被拒絕,從而可以採取相應的措施,比如增加線程池大小、優化任務執行效率或者通知用户等待。

2. CallerRunsPolicy

CallerRunsPolicy是Java線程池中的一種拒絕策略,當線程池中的線程數達到其最大容量,並且工作隊列也滿了,無法再接受新的任務時,使用CallerRunsPolicy策略會將任務交由調用者線程(即提交任務的線程)來執行。如果調用者線程已經在執行一個任務,則會創建一個新線程來執行被拒絕的任務。

業務場景

假設有一個在線視頻處理服務,用户上傳視頻後,服務需要對視頻進行轉碼、壓縮等處理。在某些情況下,如果視頻處理任務過多,線程池可能會達到其最大容量,此時使用CallerRunsPolicy策略可以保證任務不會被丟棄,而是在調用者線程中執行,從而確保所有上傳的視頻都能得到處理。

示例代碼

下面是一個使用CallerRunsPolicy策略的線程池示例代碼:

import java.util.concurrent.*;

public class CallerRunsPolicyDemo {
    public static void main(String[] args) {
        // 創建一個固定大小為2的線程池
        int numberOfThreads = 2;
        ExecutorService executor = new ThreadPoolExecutor(
            numberOfThreads,
            numberOfThreads,
            0L,
            TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>(),
            new ThreadPoolExecutor.CallerRunsPolicy() // 設置拒絕策略為CallerRunsPolicy
        );

        // 提交4個任務到線程池
        for (int i = 0; i < 4; i++) {
            int finalI = i;
            executor.submit(() -> {
                System.out.println("Task " + finalI + " is running.");
                // 模擬任務執行時間
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        // 嘗試提交第5個任務,此時線程池和隊列已滿
        executor.submit(() -> {
            System.out.println("Task 5 is running in the caller thread.");
        });

        // 關閉線程池
        executor.shutdown();
    }
}

V哥來解釋一下

在這個示例中,我們創建了一個固定大小為2的線程池,並且使用了CallerRunsPolicy作為拒絕策略。我們提交了4個任務,每個任務簡單地打印一條消息並休眠1秒。

當嘗試提交第5個任務時,由於線程池中的線程數和隊列都已滿,任務無法被線程池中的線程執行。此時,根據CallerRunsPolicy策略,任務將由提交任務的線程(即main線程)來執行。因此,你會看到"Task 5 is running in the caller thread."這條消息被打印出來。

這種策略適合於那些可以容忍任務在調用者線程中執行的業務場景,它允許任務繼續執行,而不會因為線程池資源不足而被丟棄。但是,需要注意的是,如果調用者線程本身就很忙,或者任務執行時間很長,這可能會導致調用者線程被阻塞,從而影響系統的響應性。

3. DiscardPolicy

DiscardPolicy是Java線程池中的一種拒絕策略,它在任務無法被線程池執行時,會直接丟棄該任務,不執行也不拋出任何異常。

業務場景

假設有一個日誌收集系統,該系統負責收集來自多個服務的日誌信息。由於日誌信息量巨大,線程池可能很快就會達到其最大容量,並且工作隊列也會被填滿。在這種情況下,使用DiscardPolicy策略可以避免系統因為嘗試處理大量日誌信息而變得不穩定或崩潰。對於日誌信息來説,丟棄一些信息可能是可接受的,因為它們可以稍後通過其他方式重新收集或恢復。

示例代碼

下面是一個使用DiscardPolicy策略的線程池示例代碼:

import java.util.concurrent.*;

public class DiscardPolicyDemo {
    public static void main(String[] args) {
        // 創建一個固定大小為2的線程池
        int numberOfThreads = 2;
        ExecutorService executor = new ThreadPoolExecutor(
            numberOfThreads,
            numberOfThreads,
            0L,
            TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>(2), // 限制隊列大小為2
            new ThreadPoolExecutor.DiscardPolicy() // 設置拒絕策略為DiscardPolicy
        );

        // 提交5個任務到線程池
        for (int i = 0; i < 5; i++) {
            int finalI = i;
            executor.submit(() -> {
                System.out.println("Task " + finalI + " is running.");
                // 模擬任務執行時間
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        // 由於線程池和隊列已滿,提交的第3個任務將被丟棄,不打印任何消息
    }
}

V哥來解釋一下

在這個示例中,我們創建了一個固定大小為2的線程池,並且設置了工作隊列的大小為2。這意味着線程池最多隻能同時執行2個任務,並且隊列中最多隻能有2個等待執行的任務。

我們提交了5個任務,每個任務簡單地打印一條消息並休眠1秒。當提交第3個任務時,線程池的線程數和隊列都已滿,根據DiscardPolicy策略,這個任務將被丟棄,不會有任何異常拋出,也不會有消息打印出來。

這種策略適合於那些對任務執行的及時性要求不高,或者任務可以被安全丟棄的業務場景。例如,在日誌收集、數據監控、非關鍵性消息處理等場景中,使用DiscardPolicy可以避免系統因為處理大量任務而變得不穩定。然而,需要注意的是,使用這種策略可能會導致數據丟失或任務未被執行,因此在決定使用DiscardPolicy之前,需要仔細考慮業務需求和潛在的影響。

4. DiscardOldestPolicy

DiscardOldestPolicy是Java線程池中的一種拒絕策略,當線程池中的線程數達到其最大容量,並且工作隊列也滿了,無法再接受新的任務時,使用DiscardOldestPolicy策略會從隊列中丟棄最舊的任務(即隊列頭部的任務),然後嘗試再次提交當前任務。

業務場景

假設有一個實時數據處理系統,該系統需要處理來自傳感器的實時數據流。在這種情況下,系統可能更傾向於處理最新的數據,而不是舊的數據,因為最新的數據對於分析和決策更為重要。使用DiscardOldestPolicy策略,系統可以丟棄舊的數據任務,以確保有足夠的資源來處理最新的數據。

示例代碼

下面是一個使用DiscardOldestPolicy策略的線程池示例代碼:

import java.util.concurrent.*;

public class DiscardOldestPolicyDemo {
    public static void main(String[] args) {
        // 創建一個固定大小為2的線程池
        int numberOfThreads = 2;
        ExecutorService executor = new ThreadPoolExecutor(
            numberOfThreads,
            numberOfThreads,
            0L,
            TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>(2), // 限制隊列大小為2
            new ThreadPoolExecutor.DiscardOldestPolicy() // 設置拒絕策略為DiscardOldestPolicy
        );

        // 提交5個任務到線程池
        for (int i = 0; i < 5; i++) {
            final int taskNumber = i;
            executor.submit(() -> {
                System.out.println("Task " + taskNumber + " is running.");
                // 模擬任務執行時間
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        // 等待所有任務執行完畢
        executor.shutdown();
        while (!executor.isTerminated()) {
            // 等待線程池關閉
        }
        System.out.println("All tasks have been processed.");
    }
}

V哥來解釋一下

在這個示例中,我們創建了一個固定大小為2的線程池,並且設置了工作隊列的大小為2。這意味着線程池最多隻能同時執行2個任務,並且隊列中最多隻能有2個等待執行的任務。

我們提交了5個任務,每個任務簡單地打印一條消息並休眠1秒。當提交第3個任務時,線程池的線程數和隊列都已滿。根據DiscardOldestPolicy策略,隊列中的第一個任務(即任務0)將被丟棄,然後嘗試再次提交當前任務(任務3)。這樣,任務1和任務2將被執行,任務3將替換任務0的位置並被執行,而任務4和任務5將依次進入隊列並被執行。

這種策略適合於那些對最新數據或任務更為敏感的業務場景,例如實時數據處理、股票交易系統、在線遊戲服務器等。在這些場景中,丟棄舊的任務以保證新任務的執行可能是一個合理的選擇。然而,需要注意的是,使用這種策略可能會導致數據丟失或舊任務未被執行,因此在決定使用DiscardOldestPolicy之前,需要仔細考慮業務需求和潛在的影響。

最後

這些策略可以通過ThreadPoolExecutor的構造函數或setRejectedExecutionHandler方法來設置。選擇哪種策略取決於具體的應用場景和需求。兄弟們,你是如何理解線程池的拒絕策略的呢?歡迎關注【威哥愛編程】一起研究進步。技術路上,一個會走得很累,一羣人才能走得更遠。

user avatar xuezhongyu01 頭像
1 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.