Следующий класс оборачивается вокруг ThreadPoolExecutor и использует семафор для блокировки, когда рабочая очередь заполнена:
public final class BlockingExecutor {
private final Executor executor;
private final Semaphore semaphore;
public BlockingExecutor(int queueSize, int corePoolSize, int maxPoolSize, int keepAliveTime, TimeUnit unit, ThreadFactory factory) {
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>();
this.executor = new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, unit, queue, factory);
this.semaphore = new Semaphore(queueSize + maxPoolSize);
}
private void execImpl (final Runnable command) throws InterruptedException {
semaphore.acquire();
try {
executor.execute(new Runnable() {
@Override
public void run() {
try {
command.run();
} finally {
semaphore.release();
}
}
});
} catch (RejectedExecutionException e) {
// will never be thrown with an unbounded buffer (LinkedBlockingQueue)
semaphore.release();
throw e;
}
}
public void execute (Runnable command) throws InterruptedException {
execImpl(command);
}
}
Этот класс-оболочка основан на решении, приведенном в книге Брайана Гетца в книге Java Concurrency in Practice. Решение в книге принимает только два параметра конструктора: an Executor
и границу, используемую для семафора. Это показано в ответе Fixpoint. У такого подхода есть проблема: он может попасть в состояние, когда потоки пула заняты, очередь заполнена, но семафор только что выпустил разрешение. ( semaphore.release()
в блоке finally). В этом состоянии новая задача может получить только что выпущенное разрешение, но будет отклонена, поскольку очередь задач заполнена. Конечно, вы этого не хотите; вы хотите заблокировать в этом случае.
Чтобы решить эту проблему, мы должны использовать неограниченную очередь, как ясно упоминает JCiP. Семафор действует как защита, создавая эффект размера виртуальной очереди. Это имеет побочный эффект, заключающийся в том, что модуль может содержать maxPoolSize + virtualQueueSize + maxPoolSize
задачи. Это почему? Из-
semaphore.release()
за блока в finally. Если все потоки пула вызывают этот оператор одновременно, то maxPoolSize
разрешения освобождаются, позволяя одному и тому же количеству задач войти в модуль. Если бы мы использовали ограниченную очередь, она все равно была бы заполнена, что привело бы к отклонению задачи. Теперь, поскольку мы знаем, что это происходит только тогда, когда поток пула почти завершен, это не проблема. Мы знаем, что поток пула не будет блокироваться, поэтому задача скоро будет снята из очереди.
Однако вы можете использовать ограниченную очередь. Только убедитесь, что его размер равен virtualQueueSize + maxPoolSize
. Большие размеры бесполезны, семафор не позволит впустить больше элементов. Меньшие размеры приведут к отклонению задач. Вероятность отклонения задач увеличивается с уменьшением размера. Например, предположим, что вам нужен ограниченный исполнитель с maxPoolSize = 2 и virtualQueueSize = 5. Затем возьмите семафор с 5 + 2 = 7 разрешениями и фактическим размером очереди 5 + 2 = 7. Реальное количество задач, которые могут быть в отряде, тогда 2 + 5 + 2 = 9. Когда исполнитель заполнен (5 задач в очереди, 2 в пуле потоков, поэтому доступно 0 разрешений) и ВСЕ потоки пула освобождают свои разрешения, то входящие задачи могут получить ровно 2 разрешения.
Теперь решение от JCiP несколько громоздко в использовании, поскольку оно не обеспечивает соблюдение всех этих ограничений (неограниченная очередь или ограниченная этими математическими ограничениями и т. Д.). Я думаю, что это служит только хорошим примером, демонстрирующим, как вы можете создавать новые потокобезопасные классы на основе уже доступных частей, но не как полноценный, многоразовый класс. Не думаю, что последнее было намерением автора.