ExecutorService.submit: примеры (JAVA)

Материал по использованию submit() в Java
Раздел: Многопоточность, Исполнители
ExecutorService.submit(Callable task): Future

Описание метода ExecutorService.submit()

Метод ExecutorService.submit() служит для отправки задачи на выполнение в пул потоков. Он существует в нескольких перегрузках и возвращает объект Future<T>, который представляет собой результат выполнения задачи и позволяет получить значение или статус задачи асинхронно.

Основные перегрузки:

  • Future<T> submit(Callable<T> task) - принимает Callable, выполняет его и возвращает Future с результатом типа T. Исключения, выброшенные в Callable, при вызове get() будут обернуты в ExecutionException.
  • Future<?> submit(Runnable task) - принимает Runnable, возвращает Future<?>. Результирующее значение при вызове get() будет null (если не передано иное).
  • <T> Future<T> submit(Runnable task, T result) - принимает Runnable и заранее заданный результат типа T, который возвращается при вызове get() после завершения задачи.

Поведение и важные замечания:

  • Вызов submit помещает задачу в пул и возвращает Future немедленно; выполнение может быть отложено в зависимости от конфигурации пула.
  • Чтобы получить результат, требуется вызвать future.get(). Этот вызов блокирует текущий поток, пока результат не станет доступен, или до тех пор, пока не будет выброшено исключение (InterruptedException или ExecutionException).
  • Future поддерживает методы cancel(), isDone(), isCancelled(). Отмена задачи может быть принудительной, если передан флаг прерывания.
  • Если пул завершает работу (shutdown) и новые задачи не принимаются, вызов submit приведет к выбрасыванию RejectedExecutionException в момент отправки.
  • При использовании с ForkJoinPool некоторые задаче оформляются как ForkJoinTask, но семантика submit остаётся прежней - возвращается Future.

Аргументы и возвращаемые значения подробно:

  • Аргумент: Callable<T> - функциональный интерфейс, метод call() возвращает T и может выбрасывать исключение.
  • Аргумент: Runnable - функциональный интерфейс, метод run() ничего не возвращает и не допускает проверяемых исключений; если нужно вернуть значение, используется перегрузка с параметром result.
  • Возвращаемое значение: Future<T> - предоставляет доступ к результату, к статусу и методам отмены. Значение T доступно через get(), либо null при submit(Runnable).

Ключевые сценарии использования: запуск задач с результатом, накопление результатов параллельно, синхронизация потоков через получение результата, управление отменой и таймаутами через API Future.

Короткие примеры использования

Пример 1 - submit с Callable, получение результата:

ExecutorService es = Executors.newFixedThreadPool(2);
Future<Integer> f = es.submit(() -> {
    Thread.sleep(100);
    return 42;
});
System.out.println(f.get());
es.shutdown();
42

Пример 2 - submit с Runnable без результата:

ExecutorService es = Executors.newSingleThreadExecutor();
Future<?> f = es.submit(() -> System.out.println("Hello from runnable"));
f.get(); // дождаться завершения
es.shutdown();
Hello from runnable
(null)

Пример 3 - submit с Runnable и заранее заданным результатом:

ExecutorService es = Executors.newCachedThreadPool();
Future<String> f = es.submit(() -> System.out.println("Task executed"), "OK");
System.out.println(f.get());
es.shutdown();
Task executed
OK

Пример 4 - отмена задачи через Future:

ExecutorService es = Executors.newFixedThreadPool(1);
Future<?> f = es.submit(() -> {
    try { Thread.sleep(5000); } catch (InterruptedException e) { System.out.println("Interrupted"); }
});
Thread.sleep(100);
f.cancel(true);
System.out.println("isCancelled=" + f.isCancelled() + ", isDone=" + f.isDone());
es.shutdownNow();
Interrupted
isCancelled=true, isDone=true

Похожие API в Java и их особенности

  • execute(Runnable) (интерфейс Executor) - принимает Runnable и не возвращает Future. Подходит, когда результат и управление задачей не требуются.
  • invokeAll(Collection<Callable<T>>) - отправляет набор Callable и блокирует до получения всех результатов. Удобно для батчевых операций, когда требуется дождаться каждого результата.
  • invokeAny(Collection<Callable<T>>) - возвращает результат первой успешно завершившейся задачи, остальные могут быть отменены. Полезно при параллельном поиске первого успешного ответа.
  • CompletableFuture.supplyAsync() - высокоуровневый API для композиции асинхронных операций, цепочек и обработки исключений, более удобен для сложной логики, чем низкоуровневые Future.
  • ForkJoinPool.invoke() - эффективен для рекурсивных задач, использующих алгоритм 'разделяй и властвуй'. Для таких задач ForkJoinPool часто предпочтительнее стандартного ThreadPoolExecutor.

Выбор зависит от целей: если требуется только запуск задачи без результата, выбрать execute; для получения всех результатов подряд - invokeAll; для более гибкой асинхронной композиции - CompletableFuture.

Аналоги в других языках и примеры

Краткое сравнение и примеры кода с результатом.

Python - concurrent.futures.ThreadPoolExecutor.submit:

from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=2) as ex:
    f = ex.submit(lambda: 7 * 6)
    print(f.result())
42

JavaScript - Promise и worker threads (на сервере). Пример с Promise и setTimeout:

const task = () => new Promise(res => setTimeout(() => res(42), 100));
task().then(console.log);
42

C# - Task.Run (асинхронный запуск):

using System;
using System.Threading.Tasks;

var t = Task.Run(() => 42);
Console.WriteLine(t.Result);
42

Go - goroutine и канал для получения результата:

package main
import "fmt"
func main(){
    ch := make(chan int)
    go func(){ ch <- 42 }()
    fmt.Println(<-ch)
}
42

Kotlin - корутины (аналог асинхронного выполнения):

import kotlinx.coroutines.*
fun main() = runBlocking {
    val d = async { 6 * 7 }
    println(d.await())
}
42

PHP - параллельные расширения, пример с параллель (PECL) или с pthreads (в CLI):

// пример с параллельным расширением
$runtime = new parallel\Runtime();
$future = $runtime->run(function(){ return 6*7; });
echo $future->value();
42

Lua - корутины не используют системные потоки; для реального параллелизма требуются внешние библиотеки (luaproc и т.п.). Простой пример с coroutine:

co = coroutine.create(function() coroutine.yield(42) end)
coroutine.resume(co)
print(coroutine.resume(co)) -- дважды для показа
true	42
true	nil

SQL - в классическом SQL нет аналога; базы данных поддерживают фоновые задания и планировщики (например, pg_cron в PostgreSQL), но модель отлична от ExecutorService.

Основные отличия от Java ExecutorService:

  • Нативные механизмы в других языках часто менее формализованы и зависят от фреймворка или библиотеки.
  • Go и Kotlin предлагают лёгкие конкурентные примитивы (goroutine, coroutine), которые более легковесны по сравнению с потоками JVM.
  • JavaScript использует event-loop и промисы, что отличается от многопоточности; Worker threads дает более близкую модель, но менее распространена.

Типичные ошибки при использовании

  • Ожидание результата без обработки исключений: при вызове get() возможно получение ExecutionException или InterruptedException. Пример:
ExecutorService es = Executors.newSingleThreadExecutor();
Future<Integer> f = es.submit(() -> { throw new RuntimeException("fail"); });
try { System.out.println(f.get()); } catch(Exception e) { e.printStackTrace(); }
es.shutdown();
java.util.concurrent.ExecutionException: java.lang.RuntimeException: fail
    at ...
Caused by: java.lang.RuntimeException: fail
    at ...
  • Не выполнение shutdown пула - приводит к утечкам потоков и завершению процесса, ожидающему завершения. Частая ошибка в тестах и демо-коде.
  • Неправильная отмена: при вызове cancel(false) задача не будет прервана, если она заблокирована; ожидание прерывания требует cancel(true) и корректной обработки InterruptedException внутри задачи.
  • Submit null - вызов submit(null) приводит к NullPointerException при попытке отправки.
  • Непонимание семантики submit(Runnable) - результат get() может быть null и не указывать на полезное значение.
  • Вызов get() без таймаута в критическом потоке UI или сетевом потоке, приводящий к блокировке интерфейса или зависанию сервиса.

Изменения и эволюция API

Интерфейс ExecutorService и метод submit() существуют с Java 5 и остаются стабильными. Последние ключевые изменения в экосистеме конкурентности JVM:

  • Появление CompletableFuture (Java 8) - более мощный инструмент для композиции асинхронных операций, дополняющий классический Future.
  • Развитие ForkJoinPool и поддержка параллельных стримов - альтернативные модели выполнения задач.
  • Проект Loom и виртуальные потоки (preview/GA в более поздних версиях JDK) привнесли фабрики потоков, например Executors.newVirtualThreadPerTaskExecutor, что влияет на выбор реализаций ExecutorService для высокопараллельных задач, но метод submit сам по себе не изменился.

Расширенные и редкие сценарии применения

1) Обработка результатов по мере завершения задач с помощью ExecutorCompletionService:

Пример java
ExecutorService es = Executors.newFixedThreadPool(3);
CompletionService<String> cs = new ExecutorCompletionService<>(es);
cs.submit(() -> { Thread.sleep(300); return "A"; });
cs.submit(() -> { Thread.sleep(100); return "B"; });
cs.submit(() -> { Thread.sleep(200); return "C"; });
for (int i = 0; i < 3; i++) {
    Future<String> f = cs.take(); // блокирует до следующего завершения
    System.out.println(f.get());
}
es.shutdown();
B
C
A

2) Инициализация ThreadPoolExecutor с кастомной стратегией отклонения и отправка задач через submit:

Пример java
ThreadPoolExecutor tpe = new ThreadPoolExecutor(
    1, 2, 60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(1),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.CallerRunsPolicy());

for (int i = 0; i < 5; i++) {
    final int id = i;
    tpe.submit(() -> {
        System.out.println("Task " + id + " running");
        Thread.sleep(200);
        return null;
    });
}

tpe.shutdown();
Task 0 running
Task 1 running
Task 2 running
Task 3 running
Task 4 running

Пояснение: при переполнении очереди и занятии всех потоков политика CallerRunsPolicy заставляет вызывающий поток выполнить задачу, что снижает скорость отправки и предотвращает отказ.

3) Комбинация submit и CompletableFuture для асинхронной композиции:

Пример java
ExecutorService es = Executors.newFixedThreadPool(2);
Future<Integer> f = es.submit(() -> 21);
CompletableFuture<Integer> cf = CompletableFuture.supplyAsync(() -> {
    try { return f.get(); } catch (Exception e) { throw new RuntimeException(e); }
}, es).thenApply(x -> x * 2);
System.out.println(cf.join());
es.shutdown();
42

4) Submit на виртуальных потоках (при наличии поддержки в JVM) - полезно для большого числа коротких блокирующих задач:

Пример java
try (ExecutorService es = Executors.newVirtualThreadPerTaskExecutor()) {
    Future<String> f = es.submit(() -> {
        Thread.sleep(50);
        return "ok";
    });
    System.out.println(f.get());
}
ok

5) Обработка таймаутов и отключений через get с таймаутом и последующей отменой:

Пример java
ExecutorService es = Executors.newSingleThreadExecutor();
Future<String> f = es.submit(() -> { Thread.sleep(5000); return "done"; });
try {
    System.out.println(f.get(200, TimeUnit.MILLISECONDS));
} catch (TimeoutException te) {
    f.cancel(true);
    System.out.println("timed out and cancelled");
}
es.shutdownNow();
timed out and cancelled

6) Submit внутри задачи для создания цепочек задач - осторожно с блокировками пула (deadlock), если пул фиксирован и все потоки заняты ожиданием результатов дочерних задач.

7) Ловля ExecutionException и извлечение причины для подробной диагностики:

Пример java
Future<Integer> f = es.submit(() -> { throw new IllegalStateException("boom"); });
try { f.get(); } catch (ExecutionException ee) { System.out.println(ee.getCause().getMessage()); }
boom

Описанные расширенные примеры показывают, как применять submit для тонкой настройки поведения, обработки результатов по мере доступности, обеспечения отказоустойчивости и интеграции с современными асинхронными абстракциями JVM.

джава ExecutorService.submit function comments

En
ExecutorService.submit Отправляет задачу на выполнение