Для параллельных вычислений Java предоставляет интерфейс Executor, который помогает отделить описание задач от того, как они будут выполнены, как будут использоваться потоки, как будет производиться scheduling и т.д.
Определение интерфейса выглядит так:
public interface Executor {
void execute(Runnable command);
}
Интерфейс принимает задачи в виде экземпляров класса Runnable. В определенный момент времени один из потоков "подбирает" задачу и исполняет ее, вызывая метод Runnable::run. Данный интерфейс имеет множество реализаций, предназначенных для разных типов задач.
В общем случае, при выборе того, какую именно реализацию интерфейса Executor использовать, нужно ответить на следующие вопросы, исходя из особенностей задач, которые планируется исполнять:
- Сколько параллельных потоков нужно запускать по умолчанию?
- Что нужно делать с новыми задачами, если все доступные потоки заняты?
- Нужно ли ограничивать размер очереди задач и что делать, если она переполнена?
Для явного контроля всех необходимых параметров можно создать свой собственный ThreadPoolExecutor. Его конструктор выглядит следующим образом:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
Рассмотрим, что это за параметры, и как они коррелируют с описанными выше вопросами.
- int corePoolSize - число потоков, которые должны находиться в пуле, даже если они бездействуют (начальное число потоков)
- int maximumPoolSize - максимальное число потоков, которым разрешено находиться в пуле (увеличивать ли число потоков вплоть до максимального, если все доступные потоки заняты)
- long keepAliveTime, TimeUnit unit - максимальное время бездействия до завершения тех потоков, которые были созданы сверх corePoolSize (завершать ли дополнительные потоки, если они бездействуют)
- BlockingQueue<Runnable> workQueue - очередь, используемая для хранения задач, ожидающих исполнения (как хранить задачи, которые не могут быть исполнены прямо сейчас, и должен ли быть ограничен размер такой очереди)
- ThreadFactory threadFactory - фабрика, используемая для создания новых потоков
- RejectedExecutionHandler handler - обработчик, используемый в ситуации, когда достигнуты лимиты размеров пула или очереди (что делать, если мы не можем исполнить задачу)
Пакет java.util.concurrent предоставляет несколько фабричных методов, которые помогают создать пул, используя различные реализации интерфейса Executor и различные параметры инициализации.
- newFixedThreadPool(int nThreads). Создает пул, который использует фиксированное число потоков и безлимитную очередь. Потоки находятся в пуле до тех пор, пока явно не будут остановлены. Хорошо подходит для задач, интенсивно использующих вычислительные ресурсы процессора.
- newWorkStealingPool(int parallelism). Создает пул, который держит достаточное количество потоков для поддержания заданного уровня параллелизма. Фактическое количество потоков может увеличиваться и уменьшаться динамически. Пул, созданный при помощи данного фабричного метода, может уменьшить количество конфликтов путем использования нескольких очередей. Хорошо подходит для высоконагруженных сред и для рекурсивных задач.
- newSingleThreadExecutor(). Создает пул, в котором всегда есть только один поток, и который использует очередь неограниченного размера. Не имеет никаких параметров для конфигурирования и полезен только в тех случаях, когда нужно последовательное исполнение задач в определенном порядке.
- newCachedThreadPool(). Создает пул, в котором каждый раз при необходимости создаются новые потоки, но, если есть доступные потоки из уже созданных, они будут переиспользованы. Не использует очередь. Если поток бездействует в течении минуты, он будет завершен и удален из кэша. Данный пул полезно использовать в программах, в которых исполняется много короткоживущих задач.