Як змусити ThreadPoolExecutor збільшити нитки до max перед чергою?


99

Я деякий час був розчарований поведінкою за замовчуванням, ThreadPoolExecutorяка підтримує ExecutorServiceнитки пулів, якими користується так багато з нас. Цитувати з Javadocs:

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

Це означає, що якщо ви визначите пул потоків із наступним кодом, він ніколи не запустить 2-й потік, оскільки значення LinkedBlockingQueueбез обмежень.

ExecutorService threadPool =
   new ThreadPoolExecutor(1 /*core*/, 50 /*max*/, 60 /*timeout*/,
      TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(/* unlimited queue */));

Тільки якщо у вас обмежена черга і черга заповнена, починаються будь-які нитки вище основного числа. Я підозрюю, що велика кількість молодших багатопотокових програмістів Java не знають про таку поведінку ThreadPoolExecutor.

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

Мої вимоги стосуються веб-сервісу, який здійснює зворотний дзвінок, можливо, ненадійному третьому сторону.

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

Як я можу обійти це обмеження, ThreadPoolExecutorколи чергу потрібно обмежити та заповнити, перш ніж буде запущено більше ниток? Як я можу змусити його починати більше потоків перед завданнями в черзі?

Редагувати:

@Flavio дає змогу використати тимчасовий ThreadPoolExecutor.allowCoreThreadTimeOut(true)час та вихід основних ниток для використання. Я вважав це, але все ж хотів функцію основних ниток. Я не хотів, щоб кількість потоків у пулі опускалася нижче розміру ядра, якщо це можливо.


1
Зважаючи на те, що ваш приклад створює максимум 10 ниток, чи є реальна економія на використанні чогось, що росте / стискається через пул ниток фіксованого розміру?
bstempi

Хороший момент @bstempi. Кількість була дещо довільною. Я збільшив це у запитанні до 50. Не точно впевнений, скільки одночасно потоків я хочу працювати, але зараз, коли у мене є це рішення.
Сірий

1
Ой, чорт! 10 підсумків, якщо я міг би тут, точно таке ж становище, на якому я.
Євген

Відповіді:


50

Як я можу обійти це обмеження, ThreadPoolExecutorколи чергу потрібно обмежити та заповнити, перш ніж буде запущено більше ниток.

Я вважаю, що нарешті знайшов дещо елегантне (можливо, трохи гакітне) рішення цього обмеження ThreadPoolExecutor. Вона включає в себе розширення , LinkedBlockingQueueщоб він повернутися falseдо queue.offer(...)коли вже є деякі завдання , поставлені в чергу. Якщо поточні потоки не йдуть у відповідь із завданнями з черги, TPE додасть додаткові потоки. Якщо пул вже на максимальних потоках, тоді RejectedExecutionHandlerбуде викликано. Потім обробник put(...)переходить у чергу.

Безумовно, дивно писати чергу, куди offer(...)можна повертатися falseі put()ніколи не блокувати, так що це частина злому. Але це добре працює з використанням черги TPE, тому я не бачу проблем із цим.

Ось код:

// extend LinkedBlockingQueue to force offer() to return false conditionally
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>() {
    private static final long serialVersionUID = -6903933921423432194L;
    @Override
    public boolean offer(Runnable e) {
        // Offer it to the queue if there is 0 items already queued, else
        // return false so the TPE will add another thread. If we return false
        // and max threads have been reached then the RejectedExecutionHandler
        // will be called which will do the put into the queue.
        if (size() == 0) {
            return super.offer(e);
        } else {
            return false;
        }
    }
};
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1 /*core*/, 50 /*max*/,
        60 /*secs*/, TimeUnit.SECONDS, queue);
threadPool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        try {
            // This does the actual put into the queue. Once the max threads
            //  have been reached, the tasks will then queue up.
            executor.getQueue().put(r);
            // we do this after the put() to stop race conditions
            if (executor.isShutdown()) {
                throw new RejectedExecutionException(
                    "Task " + r + " rejected from " + e);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return;
        }
    }
});

За допомогою цього механізму, коли я надсилаю завдання в чергу, ThreadPoolExecutorбуде:

  1. Початково масштабуйте кількість ниток до розміру серцевини (тут 1).
  2. Запропонуйте це до черги. Якщо черга порожня, вона буде в черзі оброблятися існуючими потоками.
  3. Якщо в черзі вже є 1 або більше елементів, offer(...)заповіт повернеться хибним.
  4. Якщо помилка повертається, масштабуйте кількість потоків у пулі, поки вони не досягнуть максимального числа (тут 50).
  5. Якщо на максимумі, то він викликає RejectedExecutionHandler
  6. В RejectedExecutionHandlerтой ставить завдання в чергу для обробки першого доступного потоку в FIFO порядку.

Хоча в моєму прикладі коду вище, черга не обмежена, ви також можете визначити її як обмежену чергу. Наприклад, якщо ви додасте до 1000 ємність, LinkedBlockingQueueто це:

  1. масштабуйте нитки до макс
  2. потім чергуйте, поки не заповниться 1000 завдань
  3. потім заблокуйте абонента, доки в черзі не стане місця.

Крім того, якщо вам потрібно було скористатися offer(...)в RejectedExecutionHandlerтоді, ви можете використовувати offer(E, long, TimeUnit)метод замість Long.MAX_VALUEцього часу.

Увага:

Якщо ви очікуєте, що завдання будуть додані виконавцю після його відключення, тоді ви, можливо, захочете бути розумнішими щодо викидання RejectedExecutionExceptionз нашого звичаю, RejectedExecutionHandlerколи служба виконавця буде відключена. Дякуємо @RaduToader за вказівку на це.

Редагувати:

Іншим підключенням до цієї відповіді може стати запитання у TPE, чи є потоки в режимі очікування, і лише піддайте елементу, якщо він є. Вам доведеться скласти для цього справжній клас і додати ourQueue.setThreadPoolExecutor(tpe);метод.

Тоді ваш offer(...)метод може виглядати приблизно так:

  1. Перевірте, чи tpe.getPoolSize() == tpe.getMaximumPoolSize()в такому випадку просто зателефонуйте super.offer(...).
  2. Інше, якщо tpe.getPoolSize() > tpe.getActiveCount()тоді зателефонувати, super.offer(...)оскільки, здається, є порожні теми.
  3. В іншому випадку поверніться falseдо вилки іншої нитки.

Можливо, це:

int poolSize = tpe.getPoolSize();
int maximumPoolSize = tpe.getMaximumPoolSize();
if (poolSize >= maximumPoolSize || poolSize > tpe.getActiveCount()) {
    return super.offer(e);
} else {
    return false;
}

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


Я також боровся з тією ж проблемою, що опинився в переважаючому методі виконання. Але це справді приємне рішення. :)
Батті

Наскільки мені не подобається ідея розірвати договір Queue, щоб досягти цього, ви, звичайно, не самотні у своїй ідеї: groovy-programming.com/post/26923146865
bstempi

3
Хіба тобі тут не дивно, що перші пару завдань будуть в черзі, і лише після цього нові нитки породжують? Наприклад, якщо ваш один основний потік зайнятий одним довгим завданням, і ви дзвоните execute(runnable), то runnableвін просто додається до черги. Якщо ви телефонуєте execute(secondRunnable), то secondRunnableдодається до черги. Але тепер, якщо ви зателефонуєте execute(thirdRunnable), тоді thirdRunnableбуде запущено в новій темі. Виконання runnableі secondRunnableлише один раз thirdRunnable(або оригінальне тривале завдання) завершено.
Роберт Тупело-Шнек

1
Так, Роберт має рацію, у сильно багатопоточному середовищі іноді зростає черга, коли є вільні теми для використання. Рішення, під яким поширюється TPE - працює набагато краще. Я думаю, що пропозиція Роберта має бути позначена як відповідь, навіть якщо вищезгаданий хак цікавий
Хочеш знати все,

1
"ВідхиленийExecutionHandler" допоміг виконавцю при відключенні. Тепер вас змушують використовувати shutdownNow (), оскільки shutdown () не заважає додавати нові завдання (через реквізит)
Radu Toader

28

Встановіть розмір ядра та максимальний розмір на одне значення, і дозвольте видалити основні нитки за допомогою пулу allowCoreThreadTimeOut(true).


+1 Так, я подумав про це, але все ж хотів мати функцію core-thread. Я не хотів, щоб пул потоків переходив до 0 ниток під час спокою. Я відредагую своє запитання, щоб вказати на це. Але відмінний момент.
Сірий

Дякую! Це просто найпростіший спосіб зробити це.
Дмитро Овчинников

28

Я вже отримав дві інші відповіді на це питання, але я підозрюю, що це найкраще.

Він заснований на техніці прийнятої відповіді , а саме:

  1. Замініть offer()метод черги, щоб (іноді) повертати помилкове,
  2. що призводить ThreadPoolExecutorдо того, що або породжує новий потік, або відхиляє завдання, і
  3. встановити RejectedExecutionHandlerна насправді черзі завдання про відхилення.

Проблема полягає в тому, коли offer()слід повернути помилкове. В даний час прийнята відповідь повертається неправдивою, коли в черзі є кілька завдань, але, як я вже зазначив у своєму коментарі, це спричиняє небажані наслідки. Крім того, якщо ви завжди повертаєте помилкові, ви продовжуватимете нерестувати нові теми, навіть якщо у черзі будуть чекати теми.

Рішення полягає у використанні Java 7 LinkedTransferQueueта offer()виклику tryTransfer(). Коли є споживча нитка, яка чекає, завдання просто перейде до цієї нитки. В іншому випадку offer()повернеться хибне, і ThreadPoolExecutorволя породить нову нитку.

    BlockingQueue<Runnable> queue = new LinkedTransferQueue<Runnable>() {
        @Override
        public boolean offer(Runnable e) {
            return tryTransfer(e);
        }
    };
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 50, 60, TimeUnit.SECONDS, queue);
    threadPool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            try {
                executor.getQueue().put(r);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    });

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

Виникає проблема, коли басейн зростає до максимального розміру. Скажімо, пул масштабується до максимального розміру, і кожен потік зараз виконує завдання, коли надіслана пропозиція impl повернеться помилковою, а ThreadPoolExecutor намагається додати потікWWorker, але пул вже досяг максимального, тому запущений буде просто відхилений. Згідно з відхиленим вами ExxceHandler, він буде запропонований до черги знову, в результаті чого цей танець мавпи повториться з самого початку.
Sudheera

1
@Sudheera Я вважаю, що ви помиляєтесь. queue.offer(), тому що він насправді викликає LinkedTransferQueue.tryTransfer(), поверне помилковий і не завдасть завдання. Однак RejectedExecutionHandlerдзвінки queue.put(), які не провалюються і завдають завдання.
Роберт Тупело-Шнек

1
@ RobertTupelo-Schneck надзвичайно корисно і приємно!
Євген

1
@ RobertTupelo-Schneck Працює як шарм! Я не знаю, чому з ява не існує чогось подібного
Георгій Пеєв,

7

Примітка. Зараз я віддаю перевагу і рекомендую іншу відповідь .

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

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

public class GrowBeforeQueueThreadPoolExecutor extends ThreadPoolExecutor {
    private int userSpecifiedCorePoolSize;
    private int taskCount;

    public GrowBeforeQueueThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
        userSpecifiedCorePoolSize = corePoolSize;
    }

    @Override
    public void execute(Runnable runnable) {
        synchronized (this) {
            taskCount++;
            setCorePoolSizeToTaskCountWithinBounds();
        }
        super.execute(runnable);
    }

    @Override
    protected void afterExecute(Runnable runnable, Throwable throwable) {
        super.afterExecute(runnable, throwable);
        synchronized (this) {
            taskCount--;
            setCorePoolSizeToTaskCountWithinBounds();
        }
    }

    private void setCorePoolSizeToTaskCountWithinBounds() {
        int threads = taskCount;
        if (threads < userSpecifiedCorePoolSize) threads = userSpecifiedCorePoolSize;
        if (threads > getMaximumPoolSize()) threads = getMaximumPoolSize();
        setCorePoolSize(threads);
    }
}

Як написано, клас не підтримує зміну вказаного користувачем corePoolSize або максимальногоPoolSize після побудови, і не підтримує маніпулювання робочою чергою безпосередньо або через remove()або purge().


Мені це подобається, крім synchronizedблоків. Чи можете ви зателефонувати до черги, щоб отримати кількість завдань. А може, використовувати AtomicInteger?
Сірий

Я хотів їх уникнути, але проблема в цьому. Якщо є кількість execute()викликів в окремих потоках, кожен збирається (1) визначити, скільки потоків потрібно, (2) setCorePoolSizeдо цього числа та (3) викликати super.execute(). Якщо кроки (1) та (2) не синхронізовані, я не впевнений, як запобігти невдалому замовленню, коли ви встановите розмір пулу основного на менший номер після більшого числа. При прямому доступі до поля суперкласу це можна зробити за допомогою порівняння та встановлення, але я не бачу чистого способу зробити це в підкласі без синхронізації.
Роберт Тупело-Шнек

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

1
На жаль, я думаю, що це гірше. Припустимо , завдання 10 і 11 викликів execute(). Кожен зателефонує, atomicTaskCount.incrementAndGet()і вони отримають 10 та 11 відповідно. Але без синхронізації (над отриманням підрахунку завдань та встановленням розміру основного пулу) ви можете отримати (1) завдання 11 встановлює розмір пулу ядра на 11, (2) завдання 10 встановлює розмір пулу ядра 10, (3) завдання 10 дзвінків super.execute(), (4) завдання 11 дзвінків super.execute()і виконується.
Роберт Тупело-Шнек

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

6

У нас є підклас, ThreadPoolExecutorякий приймає додаткові creationThresholdі переорієнтовані execute.

public void execute(Runnable command) {
    super.execute(command);
    final int poolSize = getPoolSize();
    if (poolSize < getMaximumPoolSize()) {
        if (getQueue().size() > creationThreshold) {
            synchronized (this) {
                setCorePoolSize(poolSize + 1);
                setCorePoolSize(poolSize);
            }
        }
    }
}

можливо, це теж допомагає, але ваш, звичайно, виглядає більш витонченим ...


Цікаво. Дякую за це. Я насправді не знав, що розмір серцевини змінюється.
Сірий

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

4

Рекомендована відповідь вирішує лише один (1) питання з пулом потоків JDK:

  1. Пули ниток JDK є упередженими у напрямку черги. Тож замість нерестування нової нитки вони поставлять у чергу завдання. Тільки якщо черга досягне межі, пул потоків породить нову нитку.

  2. Відкликання нитки не відбувається, коли навантаження полегшується. Наприклад, якщо у нас вибух робочих місць потрапляє в пул, що призводить до того, що пул переходить до максимуму з подальшим навантаженням максимум 2 завдання одночасно, пул буде використовувати всі потоки для обслуговування легкого навантаження, що запобігає відходу нитки. (знадобиться лише 2 нитки ...)

Невдоволений поведінкою вище, я пішов вперед і застосував пул для подолання вищевказаних недоліків.

Для вирішення проблеми 2) Використання планування Lifo вирішує проблему. Ця ідея була представлена ​​Бен Маурером на практичній конференції ACM 2015: шкала системи @ Facebook

Так народилася нова реалізація:

LifoThreadPoolExecutorSQP

Поки ця реалізація покращує ефективність виконання асинхронізації для ZEL .

Реалізація спрощена, щоб зменшити накладні перемикання контексту, забезпечуючи кращі показники для певних випадків використання.

Сподіваюся, це допомагає ...

PS: Реалізація JDK Fork Join Pool реалізує ExecutorService і працює як "нормальний" пул потоків, реалізація є ефективною, вона використовує планування потоків LIFO, однак немає контролю над розміром внутрішньої черги, таємницею виходу на пенсію ..., а головне, завдання не можуть бути переривається при їх скасуванні


1
Шкода, що ця реалізація має стільки зовнішніх залежностей. Зробити це марним для мене: - /
Мартін Л.

1
Це дійсно вдалий момент (2-й). На жаль, реалізація не зрозуміла від зовнішніх залежностей, але все ж може бути прийнята, якщо ви хочете.
Олексій Власов

1

Примітка. Зараз я віддаю перевагу і рекомендую іншу відповідь .

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

Оскільки робочі потоки можуть LinkedBlockingQueueопитувати на нове завдання, можливо, завдання буде виконано, навіть якщо є доступний потік. Щоб уникнути нерестування нових потоків навіть тоді, коли є доступні нитки, нам потрібно слідкувати за тим, скільки потоків чекають нових завдань у черзі, і лише нерестовити новий потік, коли в черзі більше завдань, ніж очікування.

final Runnable SENTINEL_NO_OP = new Runnable() { public void run() { } };

final AtomicInteger waitingThreads = new AtomicInteger(0);

BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>() {
    @Override
    public boolean offer(Runnable e) {
        // offer returning false will cause the executor to spawn a new thread
        if (e == SENTINEL_NO_OP) return size() <= waitingThreads.get();
        else return super.offer(e);
    }

    @Override
    public Runnable poll(long timeout, TimeUnit unit) throws InterruptedException {
        try {
            waitingThreads.incrementAndGet();
            return super.poll(timeout, unit);
        } finally {
            waitingThreads.decrementAndGet();
        }
    }

    @Override
    public Runnable take() throws InterruptedException {
        try {
            waitingThreads.incrementAndGet();
            return super.take();
        } finally {
            waitingThreads.decrementAndGet();
        }
    }
};

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 50, 60, TimeUnit.SECONDS, queue) {
    @Override
    public void execute(Runnable command) {
        super.execute(command);
        if (getQueue().size() > waitingThreads.get()) super.execute(SENTINEL_NO_OP);
    }
};
threadPool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        if (r == SENTINEL_NO_OP) return;
        else throw new RejectedExecutionException();            
    }
});

0

Найкраще рішення, яке я можу придумати, - це продовжити.

ThreadPoolExecutorпропонує кілька методів гачка: beforeExecuteі afterExecute. У своєму розширенні ви можете підтримувати використання обмеженої черги для подачі у завданнях та другу необмежену чергу для обробки переповнення. Коли хтось телефонує submit, ви можете спробувати помістити запит у обмежену чергу. Якщо ви зіткнулися з винятком, ви просто дотримаєте завдання у черзі переповнення. Потім ви можете використовуватиafterExecute гачок, щоб побачити, чи є щось у черзі переповнення після завершення завдання. Таким чином, виконавець подбає про речі в її обмеженій черзі спочатку і автоматично витягне з цієї необмеженої черги, як дозволяє час.

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


Мені не подобається таке рішення. Я майже впевнений, що ThreadPoolExecutor не був призначений для спадкування.
scottb

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

0

Примітка. Для JDK ThreadPoolExecutor, коли ви маєте обмежену чергу, ви створюєте нові потоки лише тоді, коли пропозиція повертається помилково. Ви можете отримати щось корисне з CallerRunsPolicy, що створює трохи BackPressure і безпосередньо дзвінки виконуються в потоці виклику.

Мені потрібно виконати завдання з потоків, створених пулом, і мати окрему чергу для планування, тоді як кількість потоків у пулі може зростати або зменшуватися між corePoolSize та максимальнимPoolSize так ...

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

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

private final AtomicInteger activeWorkers = new AtomicInteger(0);
private volatile double threshold = 0.7d;

protected void beforeExecute(Thread t, Runnable r) {
    activeWorkers.incrementAndGet();
}
protected void afterExecute(Runnable r, Throwable t) {
    activeWorkers.decrementAndGet();
}
public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();

        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }

        if (isRunning(c) && this.workQueue.offer(command)) {
            int recheck = this.ctl.get();
            if (!isRunning(recheck) && this.remove(command)) {
                this.reject(command);
            } else if (workerCountOf(recheck) == 0) {
                this.addWorker((Runnable) null, false);
            }
            //>>change start
            else if (workerCountOf(recheck) < maximumPoolSize //
                && (activeWorkers.get() > workerCountOf(recheck) * threshold
                    || workQueue.size() > workerCountOf(recheck) * threshold)) {
                this.addWorker((Runnable) null, false);
            }
            //<<change end
        } else if (!this.addWorker(command, false)) {
            this.reject(command);
        }
    }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.