Неможливо зробити кешований пул ниток з обмеженням розміру?


127

Здається, неможливо зробити кешований пул потоків з обмеженням кількості потоків, які він може створити.

Ось як статичний Executors.newCchedThreadPool реалізований у стандартній бібліотеці Java:

 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>());

У результаті всі потоки будуть виконані послідовно. Тобто пул потоків ніколи не складе більше ніж один потік для обробки ваших завдань.

Це помилка в методі Execute ThreadPoolExecutor? А може, це навмисно? Або є якийсь інший шлях?

Редагувати: я хочу щось точно схоже на пул кешованих ниток (він створює нитки на вимогу, а потім вбиває їх через деякий час очікування), але з обмеженням кількості потоків, які він може створити, і можливість продовжувати чергувати додаткові завдання після того, як у нього з'явиться досягти межі потоку. Відповідно до відповіді sjlee, це неможливо. Дивлячись на метод Execute () ThreadPoolExecutor, це справді неможливо. Мені потрібно підкласифікувати ThreadPoolExecutor і переосмислити Execute (), як SwingWorker, але те, що SwingWorker робить у його Execute (), є повним хаком.


1
Яке Ваше запитання? Чи не ваш другий приклад коду - відповідь на вашу назву?
rsp

4
Я хочу, щоб пул потоків додав потоки на вимогу у міру зростання кількості завдань, але ніколи не додасть більше ніж деяка максимальна кількість потоків. CachedThreadPool вже робить це, за винятком того, що він додасть необмежену кількість потоків і не зупиниться на деякому попередньо визначеному розмірі. Розмір, який я визначаю в прикладах, становить 3. Другий приклад додає 1 потік, але не додає ще двох, оскільки надходять нові завдання, поки інші завдання ще не виконані.
Метт Крінклау-Фогт

Перевірте це, це вирішує, debuggingisfun.blogspot.com/2012/05/…
етан

Відповіді:


235

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 - це виняток із правила, наведеного вище. Завдяки Steve для чого , що мою увагу.


4
Ви повинні написати кілька слів про метод, allowCoreThreadTimeOutщоб зробити цю відповідь ідеальною. Дивіться відповідь @ user1046052
hsestupin

1
Чудова відповідь! Ще один момент, який слід додати: інші політики відхилення також варто згадати. Дивіться відповідь @brianegge
Джефф

1
Якщо поведінка 2 не повинна говорити: "Якщо досягнуто розміру maxThread і немає порожніх потоків, вони ставлять черги із завданнями". ?
Золтан

1
Не могли б ви детальніше пояснити, що означає розмір черги? Чи означає це, що перед відхиленням можна поставити в чергу лише 20 завдань?
Золтан

1
@ Zoltán Я писав це деякий час тому, тому є ймовірність, що певна поведінка могла змінитися з тих пір (я не слідкував за останніми діями занадто уважно), але якщо припустити, що така поведінка не змінюється, №2 є правильним, як зазначено, і це мабуть, найважливіший (і дещо дивовижний) пункт цього. Як тільки розмір ядра буде досягнутий, TPE надає перевагу черзі над створенням нових потоків. Розмір черги - це буквально розмір черги, переданої до TPE. Якщо черга стає повною, але вона не досягла максимального розміру, вона створить новий потік (не відхиляє завдання). Див. №3. Сподіваюся, що це допомагає.
sjlee

60

Якщо я щось не пропустив, рішення оригінального питання просте. Наступний код реалізує бажану поведінку, як описано в оригінальному плакаті. Він породжує до 5 потоків для роботи над необмеженою чергою, а холості потоки припиняться через 60 секунд.

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

1
Ви праві. Цей метод був доданий в jdk 1.6, тому про нього знає не так багато людей. Крім того, ви не можете мати розмір основного пулу "min", що шкода.
jtahlborn

4
Я хвилююся з цього приводу (з документа JDK 8): "Коли в заданому виконанні методу (Runnable) подано нове завдання, і працює менше, ніж потоки corePoolSize, створюється новий потік для обробки запиту, навіть якщо інший працівник нитки простоюються ".
veegee

Досить впевнений, що це насправді не працює. Минулого разу я дивився, що робиться вище, насправді лише коли-небудь виконує вашу роботу в одній нитці, навіть незважаючи на те, що ви породжували 5. Знову пройшло кілька років, але коли я занурювався в реалізацію ThreadPoolExecutor, він лише надсилав нові теми, коли ваша черга була заповнена. Використання необмеженої черги призводить до того, що цього ніколи не відбувається. Ви можете протестувати, подавши роботу та увійдіть у ім’я нитки, а потім вимкнено. Кожен випускний файл в кінцевому підсумку надрукує однойменну назву / не виконуватиметься на будь-якому іншому потоці.
Метт Крінклау-Фогт

2
Це справді працює, Метт. Ви встановлюєте розмір ядра 0, тому у вас було лише 1 нитка. Хитрість тут полягає в тому, щоб встановити розмір ядра на максимальний розмір.
Т-Гергелі

1
@vegee має рацію - насправді це не дуже добре працює - ThreadPoolExecutor повторно використовуватиме нитки лише тоді, коли вище corePoolSize. Отже, коли corePoolSize дорівнює maxPoolSize, ви отримаєте користь від кешування ниток лише тоді, коли ваш пул буде заповнений (Отже, якщо ви збираєтесь використовувати це, але зазвичай залишаєтеся під максимальним розміром пулу, ви можете також зменшити час очікування потоку до низького значення, і врахуйте, що немає кешування - завжди нові теми)
Кріс Рідделл

7

Мав те саме питання. Оскільки жодна інша відповідь не поєднує всі питання разом, я додаю своє:

Тепер це чітко написано в документах : Якщо ви використовуєте чергу, яка не блокує ( 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. Submit()буде кинутий, RejectedExecutionExceptionякщо кількість очікуваних завдань перевищує Integer.MAX_VALUE. Не впевнений, що спочатку у нас залишиться пам'ять, або це станеться.

  3. Можливо 4 основних потоків. Прості основні потоки автоматично виходять з режиму очікування протягом 5 секунд. Так, так, суворо за потребою потоки. Кількість можна змінити за допомогою setThreads()методу.

  4. Переконайтесь, що мінімальна кількість основних потоків ніколи не менше однієї, інакше submit()відхилить кожне завдання. Оскільки основними потоками повинні бути> = max потоки, метод також setThreads()встановлює max потоки, хоча налаштування max потоку марно для безмежної черги.


Я думаю, вам також потрібно встановити 'enableCoreThreadTimeOut' на 'true', інакше, як тільки нитки будуть створені, ви збережете їх навколо назавжди: gist.github.com/ericdcobb/46b817b384f5ca9d5f5d
eric

ой, я просто пропустив це, вибачте, ваша відповідь тоді ідеальна!
eric

6

У вашому першому прикладі наступні завдання відхиляються, оскільки AbortPolicyце за замовчуванням RejectedExecutionHandler. ThreadPoolExecutor містить такі політики, які можна змінити за допомогою setRejectedExecutionHandlerметоду:

CallerRunsPolicy
AbortPolicy
DiscardPolicy
DiscardOldestPolicy

Це здається, що вам потрібно кешований пул потоків з CallerRunsPolicy.


5

Жоден з відповідей тут не вирішив мою проблему, яка була пов'язана зі створенням обмеженої кількості HTTP-з'єднань за допомогою HTTP-клієнта Apache (версія 3.x). Оскільки мені знадобилося кілька годин, щоб з'ясувати гарне налаштування, я поділюся:

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

Це створює a ThreadPoolExecutor що починається з п'яти і вміщує максимум десять одночасно запущених потоків, що використовуються CallerRunsPolicyдля виконання.


Проблема цього рішення полягає в тому, що якщо ви збільшите кількість або виробників, ви збільшите кількість потоків, що працюють на фонових потоках. У багатьох випадках це не те, що ви хочете.
Сірий

3

Відповідно до Javadoc для ThreadPoolExecutor:

Якщо запущено більше, ніж corePoolSize, але менше максимального потокуPoolSize, новий потік буде створений, лише якщо черга заповнена . Встановивши corePoolSize та максимальнийPoolSize того самого, ви створюєте пул потоків фіксованого розміру.

(Наголос мій.)

Відповідь джиттера - це те, що ви хочете, хоча моя відповідає на ваше інше питання. :)


2

є ще один варіант. Замість використання нового SynchronousQueue ви можете також використовувати будь-яку іншу чергу, але ви повинні переконатися, що її розмір дорівнює 1, так що змусить виконавець служби створити новий потік.


Я думаю, ви маєте на увазі розмір 0 (за замовчуванням), щоб не було черги завдань і по-справжньому змушувати виконавцю служби кожен раз створювати нову нитку.
Леонмакс

2

Не схоже, що будь-який з відповідей насправді відповідає на питання - насправді я не бачу способу цього зробити, - навіть якщо ви підклас з 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 не перевірено вище


2

Ось ще одне рішення. Я думаю, що це рішення поводиться так, як ви цього хочете (хоча цим рішенням не пишаюся):

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);

2

Це те, що ти хочеш (принаймні, мабуть, так). Для пояснення перевірте відповідь Джонатана Фейнберга

Executors.newFixedThreadPool(int n)

Створює пул потоків, який повторно використовує фіксовану кількість потоків, що працюють поза спільною необмеженою чергою. У будь-якій точці, щонайбільше nThreads буде активним завданням обробки. Якщо додаткові завдання будуть подані, коли всі потоки активні, вони будуть чекати в черзі, поки нитка не буде доступною. Якщо будь-який потік припиняється через збій під час виконання перед відключенням, новий зайняє своє місце, якщо потрібно для виконання подальших завдань. Нитки в пулі існуватимуть до тих пір, поки воно не буде явно закрито.


4
Звичайно, я міг би використовувати фіксований пул потоків, але це залишило б n ниток назавжди або до того моменту, коли я закличу завершення роботи. Я хочу щось саме схоже на пул кешованих потоків (він створює нитки на вимогу, а потім вбиває їх через деякий час), але з обмеженням кількості потоків, які він може створити.
Метт Крінклау-Фогт

0
  1. Ви можете використовувати, ThreadPoolExecutorяк запропонував @sjlee

    Ви можете динамічно контролювати розмір пулу. Погляньте на це питання для більш детальної інформації:

    Динамічний басейн з нитками

    АБО

  2. Ви можете використовувати API APIWWWSSTALINGPool , який був представлений з java 8.

    public static ExecutorService newWorkStealingPool()

    Створює пул потоків крадіжок роботи, використовуючи всі доступні процесори як його цільовий рівень паралелізму.

За замовчуванням рівень паралелізму встановлюється на кількість ядер CPU на вашому сервері. Якщо у вас є 4-х основний сервер центрального процесора, розмір пулу потоків буде дорівнює 4. Цей API повертає ForkJoinPoolтип ExecutorService і дозволяє працювати з крадінням порожніх потоків шляхом крадіжки завдань із зайнятих потоків у ForkJoinPool.


0

Проблема була зведена так:

Я хочу щось точно схоже на пул кешованих потоків (він створює нитки на вимогу, а потім вбиває їх через деякий час очікування), але з обмеженням кількості потоків, які він може створити, і можливістю продовжувати чергувати додаткові завдання, як тільки вдарив межа потоку.

Перш ніж вказати на рішення, я поясню, чому такі рішення не працюють:

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

Це не стане чергою жодних завдань, коли буде досягнуто ліміт 3, оскільки SynchronousQueue, за визначенням, не може містити жодних елементів.

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

Це не створить більше одного потоку, тому що ThreadPoolExecutor створює лише теми, що перевищують corePoolSize, якщо черга заповнена. Але черга LinkedBlockingQue ніколи не заповнена.

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

Це не використовуватиме повторно теми, поки не буде досягнуто corePoolSize, оскільки ThreadPoolExecutor збільшує кількість потоків, поки не буде досягнуто corePoolSize, навіть якщо існуючі потоки не працюють. Якщо ви можете жити з цим недоліком, то це найпростіше рішення проблеми. Це також рішення, описане в "Конкурсі Java на практиці" (виноска на с. 172).

Єдиним повним рішенням описаної проблеми, здається, є те, що включає переосмислення offerметоду черги та написання а, RejectedExecutionHandlerяк пояснено у відповідях на це питання: Як змусити ThreadPoolExecutor збільшити нитки до max перед чергою?


0

Це працює для 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.