Как справиться с обратным давлением в Java с помощью пула потоков?
Как отклонить новые задачи, чтобы было отправлено не более N задач. N - максимально допустимое количество задач в очереди на отправку, включая новые, запущенные, приостановленные (не завершенные) задачи.
Вариант использования
Пользователи отправляют расчетные задачи, которые выполняются в течение некоторого времени. Иногда так много пользователей отправляют задачи одновременно. Как отклонить новые задачи, если уже отправлено N задач.
Другими словами, общее количество отправленных задач (не завершенных, начатых или не начатых) не может превышать N.
Пример кода
Вот полная версия, а ниже приведены короткие фрагменты.
Долгоиграющая задача. Расчетная задача.
public class CalculationTask {
public CalculationTask(final String name) {
this.name = name;
}
public CalculationResult calculate() {
final long waitTimeMs = MIN_WAIT_TIME_MS + RANDOM.nextInt(MAX_WAIT_TIME_MS);
sleep(waitTimeMs);
final int result = Math.abs(RANDOM.nextInt());
final String text = "This is result: " + result;
final CalculationResult calculationResult = new CalculationResult(name, text, result);
System.out.println("Calculation finished: " + calculationResult);
return calculationResult;
}
}
Его результат. Результат расчета.
public class CalculationResult {
private final String taskName;
private final String text;
private final Integer number;
// Getters, setters, constructor, toString.
}
Я так отправляю вакансии. Расчетный брокер.
public class CalculationBroker {
private static final int MAX_WORKERS_NUMBER = 5;
private final ExecutorService executorService = Executors.newFixedThreadPool(MAX_WORKERS_NUMBER);
private final Map<String, CalculationResult> calculationCache = new ConcurrentHashMap<>();
public CompletableFuture<CalculationResult> submit(final CalculationTask calculationTask) {
final CalculationResult calculationResultCached = calculationCache.get(calculationTask.getName());
if (calculationResultCached != null) {
return CompletableFuture.completedFuture(calculationResultCached);
}
System.out.println("Calculation submitted: " + calculationTask.getName());
final CompletableFuture<CalculationResult> calculated = CompletableFuture
.supplyAsync(calculationTask::calculate, executorService);
calculated.thenAccept(this::updateCache);
return calculated;
}
private void updateCache(final CalculationResult calculationResult) {
calculationCache.put(calculationResult.getTaskName(), calculationResult);
}
}
И вот как я запускаю их вместе. Основной.
public class Main {
public static void main(String[] args) {
final int N_TASKS = 100;
final CalculationBroker calculationBroker = new CalculationBroker();
final List<CompletableFuture<CalculationResult>> completableFutures = new ArrayList<>();
for (int i = 0; i < N_TASKS; i++) {
final CalculationTask calculationTask = createCalculationTask(i);
final CompletableFuture<CalculationResult> calculationResultCompletableFuture =
calculationBroker.submit(calculationTask);
completableFutures.add(calculationResultCompletableFuture);
}
calculationBroker.close();
}
private static CalculationTask createCalculationTask(final int counter) {
return new CalculationTask("CalculationTask_" + counter);
}
}
Это выход.
2020-05-23 14:14:53 [main] INFO c.y.t.backperssure.CalculationBroker – Calculation submitted: CalculationTask_97.
2020-05-23 14:14:53 [main] INFO c.y.t.backperssure.CalculationBroker – Calculation submitted: CalculationTask_98.
2020-05-23 14:14:53 [main] INFO c.y.t.backperssure.CalculationBroker – Calculation submitted: CalculationTask_99.
2020-05-23 14:14:54 [pool-1-thread-3] INFO c.y.t.backperssure.CalculationTask – Calculation finished: CalculationResult{taskName='CalculationTask_2', text='This is result: 1081871544', number=1081871544, durationMs=1066}
2020-05-23 14:14:55 [pool-1-thread-1] INFO c.y.t.backperssure.CalculationTask – Calculation finished: CalculationResult{taskName='CalculationTask_0', text='This is result: 1942553785', number=1942553785, durationMs=1885}
2020-05-23 14:14:56 [pool-1-thread-5] INFO c.y.t.backperssure.CalculationTask – Calculation finished: CalculationResult{taskName='CalculationTask_4', text='This is result: 104326011', number=104326011, durationMs=2120}
20
Мои выводы.
Код выше эквивалентен Executors.newFixedThreadPool(n), однако вместо неограниченной LinkedBlockingQueue по умолчанию мы используем ArrayBlockingQueue с фиксированной емкостью 100. Это означает, что если 100 задач уже поставлены в очередь (и n выполняются), новая задача будет отклонена с RejectedExecutionException .
ThreadPoolExecutor
использует LinkedBlockingQueue
, который по умолчанию не ограничен.
Как следует из сообщения выше:
final BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(100);
executorService = new ThreadPoolExecutor(n, n, 0L, TimeUnit.MILLISECONDS, queue);