一位有多年開發經驗的兄弟最近正在跳槽換工作,雖然同在帝都,好幾年都沒見面了,週末約着一塊小酌一下,聊到面試被問題線程池拒絕策略的問題(木有辦法,搞技術的人,聊天不超過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方法來設置。選擇哪種策略取決於具體的應用場景和需求。兄弟們,你是如何理解線程池的拒絕策略的呢?歡迎關注【威哥愛編程】一起研究進步。技術路上,一個會走得很累,一羣人才能走得更遠。