簡體   English   中英

無法創建具有大小限制的緩存線程池?

[英]Impossible to make a cached thread pool with a size limit?

似乎不可能制作一個緩存線程池,限制它可以創建的線程數。

以下是標准 Java 庫中靜態 Executors.newCachedThreadPool 的實現方式:

 public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

因此,使用該模板繼續創建一個固定大小的緩存線程池:

new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new SynchronusQueue<Runable>());

現在,如果您使用它並提交 3 個任務,一切都會好起來的。 提交任何進一步的任務將導致被拒絕的執行異常。

試試這個:

new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runable>());

將導致所有線程順序執行。 即,線程池永遠不會產生多個線程來處理您的任務。

這是ThreadPoolExecutor 的execute 方法中的一個bug? 或者這可能是故意的? 或者有其他方法嗎?

編輯:我想要一些與緩存線程池完全一樣的東西(它按需創建線程,然后在超時后殺死它們),但它可以創建的線程數量有限制,並且一旦它有繼續排隊附加任務的能力達到其線程限制。 根據 sjlee 的回應,這是不可能的。 看ThreadPoolExecutor的execute()方法確實是不可能的。 我需要子類化 ThreadPoolExecutor 並覆蓋 execute() 有點像 SwingWorker 所做的,但 SwingWorker 在其 execute() 中所做的是一個完整的黑客。

ThreadPoolExecutor 有以下幾個關鍵行為,你的問題可以用這些行為來解釋。

提交任務時,

  1. 如果線程池未達到核心大小,則創建新線程。
  2. 如果已達到核心大小且沒有空閑線程,則將任務排隊。
  3. 如果已達到核心大小,則沒有空閑線程,並且隊列已滿,則創建新線程(直到達到最大大小)。
  4. 如果已達到最大大小,沒有空閑線程,並且隊列已滿,則拒絕策略啟動。

在第一個示例中,請注意 SynchronousQueue 的大小基本上為 0。因此,當您達到最大大小 (3) 時,拒絕策略就會啟動 (#4)。

在第二個示例中,選擇的隊列是具有無限大小的 LinkedBlockingQueue。 因此,您會被行為#2 困住。

您不能真正修改緩存類型或固定類型,因為它們的行為幾乎完全確定。

如果您想擁有一個有界且動態的線程池,則需要使用正的核心大小和最大大小以及有限大小的隊列。 例如,

new ThreadPoolExecutor(10, // core size
    50, // max size
    10*60, // idle timeout
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<Runnable>(20)); // queue with a size

附錄:這是一個相當老的答案,似乎 JDK 在核心大小為 0 時改變了它的行為。從 JDK 1.6 開始,如果核心大小為 0 並且池沒有任何線程,則 ThreadPoolExecutor 將添加一個線程來執行該任務。 因此,核心大小為 0 是上述規則的一個例外。 感謝史蒂夫我注意到這一點。

除非我遺漏了什么,否則原始問題的解決方案很簡單。 以下代碼實現了原始海報所描述的所需行為。 它將產生多達 5 個線程來處理無界隊列,空閑線程將在 60 秒后終止。

tp = new ThreadPoolExecutor(5, 5, 60, TimeUnit.SECONDS,
                    new LinkedBlockingQueue<Runnable>());
tp.allowCoreThreadTimeOut(true);

有同樣的問題。 由於沒有其他答案可以將所有問題放在一起,因此我添加了我的:

現在在文檔中清楚地寫了:如果您使用不阻塞的隊列( LinkedBlockingQueue )最大線程設置無效,則僅使用核心線程。

所以:

public class MyExecutor extends ThreadPoolExecutor {

    public MyExecutor() {
        super(4, 4, 5,TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
        allowCoreThreadTimeOut(true);
    }

    public void setThreads(int n){
        setMaximumPoolSize(Math.max(1, n));
        setCorePoolSize(Math.max(1, n));
    }

}

這個執行者有:

  1. 沒有最大線程的概念,因為我們使用的是無界隊列。 這是一件好事,因為如果執行器遵循其通常的策略,則此類隊列可能會導致執行器創建大量非核心的額外線程。

  2. 最大大小為Integer.MAX_VALUE隊列。 如果掛起的任務數超過Integer.MAX_VALUE Submit()將拋出RejectedExecutionException 不確定我們會先耗盡內存,否則會發生這種情況。

  3. 可能有 4 個核心線程。 如果空閑 5 秒,空閑的核心線程會自動退出。所以,是的,嚴格按需線程。可以使用setThreads()方法更改setThreads()

  4. 確保核心線程的最小數量永遠不會小於 1,否則submit()將拒絕每個任務。 由於核心線程需要 >= 最大線程數,因此setThreads()方法setThreads()設置了最大線程數,盡管最大線程數設置對於無界隊列是無用的。

在您的第一個示例中,后續任務被拒絕,因為AbortPolicy是默認的RejectedExecutionHandler ThreadPoolExecutor 包含以下策略,您可以通過setRejectedExecutionHandler方法更改這些setRejectedExecutionHandler

CallerRunsPolicy
AbortPolicy
DiscardPolicy
DiscardOldestPolicy

聽起來您希望使用 CallerRunsPolicy 緩存線程池。

這里的答案都沒有解決我的問題,這與使用 Apache 的 HTTP 客戶端(3.x 版本)創建有限數量的 HTTP 連接有關。 由於我花了幾個小時才找到一個好的設置,我將分享:

private ExecutorService executor = new ThreadPoolExecutor(5, 10, 60L,
  TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),
  Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());

這將創建一個ThreadPoolExecutor ,它以五個開頭,並使用CallerRunsPolicy執行最多可容納十個同時運行的線程。

根據 ThreadPoolExecutor 的 Javadoc:

如果有超過 corePoolSize 但少於 maximumPoolSize 的線程正在運行,則只有在隊列已滿時才會創建新線程。 通過將 corePoolSize 和 maximumPoolSize 設置為相同,您可以創建一個固定大小的線程池。

(強調我的。)

jitter 的答案是你想要的,雖然我的回答了你的另一個問題。 :)

這就是你想要的(至少我猜是這樣)。 有關解釋,請查看Jonathan Feinberg 的回答

Executors.newFixedThreadPool(int n)

創建一個線程池,該線程池重用固定數量的線程在共享的無界隊列中運行。 在任何時候,最多 nThreads 個線程將是活動的處理任務。 如果在所有線程都處於活動狀態時提交了其他任務,它們將在隊列中等待,直到有線程可用。 如果任何線程在關閉前的執行過程中因失敗而終止,則在需要執行后續任務時,將有一個新線程取而代之。 池中的線程將一直存在,直到它被明確關閉。

還有一種選擇。 除了使用 new SynchronousQueue,您還可以使用任何其他隊列,但您必須確保其大小為 1,以便強制 executorservice 創建新線程。

看起來好像沒有任何答案實際上回答了這個問題 - 事實上我看不到這樣做的方法 - 即使你從 PooledExecutorService 子類化,因為許多方法/屬性都是私有的,例如讓 addIfUnderMaximumPoolSize 受到保護,你可以執行以下操作:

class MyThreadPoolService extends ThreadPoolService {
    public void execute(Runnable run) {
        if (poolSize() == 0) {
            if (addIfUnderMaximumPoolSize(run) != null)
                return;
        }
        super.execute(run);
    }
}

我得到的最接近的是這個 - 但即使這樣也不是一個很好的解決方案

new ThreadPoolExecutor(min, max, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()) {
    public void execute(Runnable command) {
        if (getPoolSize() == 0 && getActiveCount() < getMaximumPoolSize()) {        
            super.setCorePoolSize(super.getCorePoolSize() + 1);
        }
        super.execute(command);
    }

    protected void afterExecute(Runnable r, Throwable t) {
         // nothing in the queue
         if (getQueue().isEmpty() && getPoolSize() > min) {
             setCorePoolSize(getCorePoolSize() - 1);
         }
    };
 };

ps沒有測試以上

這是另一種解決方案。 我認為此解決方案的行為符合您的要求(盡管對此解決方案並不感到自豪):

final LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>() {
    public boolean offer(Runnable o) {
        if (size() > 1)
            return false;
        return super.offer(o);
    };

    public boolean add(Runnable o) {
        if (super.offer(o))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }
};

RejectedExecutionHandler handler = new RejectedExecutionHandler() {         
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        queue.add(r);
    }
};

dbThreadExecutor =
        new ThreadPoolExecutor(min, max, 60L, TimeUnit.SECONDS, queue, handler);
  1. 您可以按照@sjlee 的建議使用ThreadPoolExecutor

    您可以動態控制池的大小。 看看這個問題了解更多詳情:

    動態線程池

    要么

  2. 您可以使用 java 8 中引入的newWorkStealingPool API。

     public static ExecutorService newWorkStealingPool()

    使用所有可用的處理器作為其目標並行度級別來創建竊取工作的線程池。

默認情況下,並行級別設置為服務器中的 CPU 內核數。 如果您有 4 核 CPU 服務器,線程池大小將為 4。此 API 返回ForkJoinPool類型的ExecutorService並允許通過從 ForkJoinPool 中的繁忙線程竊取任務來竊取空閑線程的工作。

問題總結如下:

我想要一些與緩存線程池完全相同的東西(它按需創建線程,然后在超時后殺死它們),但是它可以創建的線程數量有限制,並且一旦達到它就能夠繼續排隊附加任務線程限制。

在指向解決方案之前,我將解釋為什么以下解決方案不起作用:

new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new SynchronousQueue<>());

當達到 3 的限制時,這不會對任何任務進行排隊,因為 SynchronousQueue 根據定義不能容納任何元素。

new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());

這不會創建多個線程,因為 ThreadPoolExecutor 僅在隊列已滿時創建超過 corePoolSize 的線程。 但是 LinkedBlockingQueue 永遠不會滿。

ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 3, 60, TimeUnit.SECONDS,
    new LinkedBlockingQueue<Runnable>());
executor.allowCoreThreadTimeOut(true);

在達到 corePoolSize 之前,這不會重用線程,因為即使現有線程空閑,ThreadPoolExecutor 也會增加線程數,直到達到 corePoolSize。 如果你能忍受這個缺點,那么這是解決問題的最簡單的方法。 它也是“Java Concurrency in Practice”(p172 的腳注)中描述的解決方案。

所描述問題的唯一完整解決方案似乎是涉及覆蓋隊列的offer方法並編寫RejectedExecutionHandler解決方案,如以下問題的答案中所述:如何在排隊之前讓 ThreadPoolExecutor 將線程增加到最大值?

這適用於 Java8+(以及其他,目前..)

     Executor executor = new ThreadPoolExecutor(3, 3, 5, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>()){{allowCoreThreadTimeOut(true);}};

其中 3 是線程數的限制,5 是空閑線程的超時時間。

如果你想自己檢查它是否有效,這里是完成這項工作的代碼:

public static void main(String[] args) throws InterruptedException {
    final int DESIRED_NUMBER_OF_THREADS=3; // limit of number of Threads for the task at a time
    final int DESIRED_THREAD_IDLE_DEATH_TIMEOUT=5; //any idle Thread ends if it remains idle for X seconds

    System.out.println( java.lang.Thread.activeCount() + " threads");
    Executor executor = new ThreadPoolExecutor(DESIRED_NUMBER_OF_THREADS, DESIRED_NUMBER_OF_THREADS, DESIRED_THREAD_IDLE_DEATH_TIMEOUT, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>()) {{allowCoreThreadTimeOut(true);}};

    System.out.println(java.lang.Thread.activeCount() + " threads");

    for (int i = 0; i < 5; i++) {
        final int fi = i;
        executor.execute(() -> waitsout("starting hard thread computation " + fi, "hard thread computation done " + fi,2000));
    }
    System.out.println("If this is UP, it works");

    while (true) {
        System.out.println(
                java.lang.Thread.activeCount() + " threads");
        Thread.sleep(700);
    }

}

static void waitsout(String pre, String post, int timeout) {
    try {
        System.out.println(pre);
        Thread.sleep(timeout);
        System.out.println(post);
    } catch (Exception e) {
    }
}

上面代碼的輸出對我來說是

1 threads
1 threads
If this is UP, it works
starting hard thread computation 0
4 threads
starting hard thread computation 2
starting hard thread computation 1
4 threads
4 threads
hard thread computation done 2
hard thread computation done 0
hard thread computation done 1
starting hard thread computation 3
starting hard thread computation 4
4 threads
4 threads
4 threads
hard thread computation done 3
hard thread computation done 4
4 threads
4 threads
4 threads
4 threads
3 threads
3 threads
3 threads
1 threads
1 threads

我推薦使用信號方法

來自SignalExecutors類:

如果提供的隊列從 offer() 返回 false,則 ThreadPoolExecutor 只會創建一個新線程。 這意味着如果你給它一個無界隊列,它只會創建 1 個線程,不管隊列有多長。 但是,如果您綁定隊列並提交比線程數更多的可運行對象,則您的任務將被拒絕並引發異常。 因此,我們創建了一個隊列,如果它不為空,它將始終返回 false 以確保創建新線程。 然后,如果任務被拒絕,我們只需將其添加到隊列中。

    public static ExecutorService newCachedBoundedExecutor(final String name, int minThreads, int maxThreads) {
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(minThreads,
            maxThreads,
            30,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>() {
                @Override
                public boolean offer(Runnable runnable) {
                    if (size() > 1 && size() <= maxThreads) {
                        //create new thread
                        return false;
                    } else {
                        return super.offer(runnable);
                    }
                }
            }, new NumberedThreadFactory(name));

    threadPool.setRejectedExecutionHandler((runnable, executor) -> {
        try {
            executor.getQueue().put(runnable);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    });

    return threadPool;
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM