Наступний клас обертається навколо ThreadPoolExecutor і використовує Semaphore для блокування, тоді черга роботи заповнена:
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. Рішення в книзі приймає лише два конструкторські параметри: a Executor
і обмежений, що використовується для семафору. Це показано у відповіді, наданій Fixpoint. У цьому підході є проблема: він може потрапити в стан, коли нитки пулу зайняті, черга повна, але семафор щойно видав дозвіл. (semaphore.release()
в остаточному блоці). У такому стані нове завдання може захопити щойно випущений дозвіл, але його відхилено, оскільки черга завдань заповнена. Звичайно, це не те, чого ти хочеш; ви хочете заблокувати в цьому випадку.
Щоб вирішити це, ми повинні використовувати необмежену чергу, як чітко згадує JCiP. Семафор діє як охорона, надаючи ефект розміру віртуальної черги. Це побічно впливає на те, що цілком можливо, що підрозділ може містити maxPoolSize + virtualQueueSize + maxPoolSize
завдання. Чому так? Через те,
semaphore.release()
що, нарешті, блок. Якщо всі потоки пулу викликають цей вислів одночасно, тоді maxPoolSize
дозволу звільняються, дозволяючи однаковій кількості завдань входити в блок. Якби ми використовували обмежену чергу, вона все одно буде повною, що призведе до відхиленого завдання. Тепер, оскільки ми знаємо, що це відбувається лише тоді, коли нитка пулу майже виконана, це не проблема. Ми знаємо, що нитка пулу не блокується, тому незабаром буде взято завдання з черги.
Однак ви можете використовувати обмежену чергу. Просто переконайтесь, що його розмір дорівнює virtualQueueSize + maxPoolSize
. Більший розмір марний, семафор не дозволить пускати більше предметів. Менші розміри призводять до відхилених завдань. Шанс відхилення завдань збільшується зі зменшенням розміру. Наприклад, скажіть, що ви хочете обмеженого виконавця з maxPoolSize = 2 та virtualQueueSize = 5. Потім візьміть семафор з 5 + 2 = 7 дозволів і фактичним розміром черги 5 + 2 = 7. Реальна кількість завдань, які можуть бути в підрозділі, становить 2 + 5 + 2 = 9. Коли виконавець заповнений (5 завдань у черзі, 2 у пулі потоків, тому доступно 0 дозволів) та ВСІ нитки пулу випускають свої дозволи, то рівно 2 дозволу можуть бути отримані завданнями, що надходять.
Тепер рішення від JCiP є дещо громіздким у використанні, оскільки воно не виконує всіх цих обмежень (необмеженої черги або обмежених цими математичними обмеженнями тощо). Я думаю, що це слугує лише гарним прикладом для демонстрації того, як ви можете створювати нові безпечні класи для потоків на основі наявних частин, але не як повноцінного класу для багаторазового використання. Я не думаю, що останній був задумом автора.