Вплоть до Java 5 создавать потоки и управлять ими было возможно только на уровне приложения. Объекты Thread требуют значительного объема памяти. Таким образом, если постоянно создавать много таких объектов в крупномасштабном приложении, это приведет к существенным затратам памяти. Поэтому создание потоков и управление ими лучше отделить от остальной части приложения.
В качестве решения Java предоставляет фреймворк Executor. Он содержит множество функций для эффективного управления несколькими потоками. Не нужно каждый раз создавать новые потоки с помощью Executor — он позволяет использовать уже созданные потоки, когда вам это необходимо. В результате экономится как память вашего Java-приложения, так и ваше драгоценное время.
Из этой статьи вы узнаете о фреймворке Executor, пуле потоков и различных методах их создания, а также о том, как с их помощью управлять потоками. Давайте начнем.
Во фреймворке Executor присутствуют три интерфейса: Executor
, ExecutorService
и ScheduledExecutorService
.
Executor
— простой интерфейс, содержащий метод execute()
для запуска задачи, заданной запускаемым объектом Runnable
.
ExecutorService
представляет собой суб-интерфейс Executor
, который добавляет функциональность для управления жизненным циклом потоков. Он также включает в себя метод submit()
, который аналогичен методу execute()
, но более универсален. Перегруженные версии метода submit()
могут принимать как выполняемый (Runnable
), так и вызываемый (Callable
) объект. Вызываемые объекты аналогичны выполняемым, за тем исключением, что задача, определенная вызываемым объектом, также может возвращать значение. Поэтому, если мы передаем объект Callable
методу submit()
, он возвращает объект Future
. Этот объект можно использовать для получения возвращаемого значения Callable
и управления статусом как Callable
, так и Runnable
задач.
ScheduledExecutorService
, в свою очередь, — это суб-интерфейс ExecutorService
. Он добавляет функциональность, которая позволяет планировать выполнение задач в коде.
Помимо трех вышеупомянутых интерфейсов, Executor Framework также содержит класс Executors
, который по умолчанию включает в себя методы для создания различных типов служб-исполнителей. С помощью этого класса и интерфейсов можно создавать пулы потоков. Что же это такое?
Пул потоков — это набор объектов Runnable
и постоянно работающих потоков. Коллекция объектов Runnable
называется рабочей очередью. Постоянно запущенные потоки проверяют рабочий запрос на наличие новой работы, и если новая работа должна быть выполнена, то из рабочей очереди будет запущен объект Runnable
. Чтобы использовать фреймворк Executor, нам нужно создать пул потоков и отправить туда задачу для выполнения. В классе Executors
есть четыре основных метода, которые используются для создания пулов потоков. Рассмотрим каждый из них на примере.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyRunnable implements Runnable {
private final String task;
MyRunnable(String task) {
this.task = task;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("Executing "+ task +" with "+Thread.currentThread().getName());
}
System.out.println();
}
}
public class Exec1 {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
for (int i = 1; i <= 5; i++) {
Runnable worker = new MyRunnable("Task" + i);
executor.execute(worker);
}
executor.shutdown(); /* После этого исполнитель перестанет принимать какие-либо новые потоки и завершит все существующие в очереди */
}
}
В данном примере мы отправляем на исполнение пять задач. Но так как применяется метод newSingleThreadExecutor()
, будет создан только один новый поток и одновременно будет выполняться только одна задача. Остальные четыре задачи находятся в очереди ожидания. Как только задача выполнится потоком, этот поток тут же выберет и выполнит следующую. Метод shutdown()
ожидает завершения выполнения задач, в настоящий момент переданных исполнителю, чтобы завершить его работу. Однако, если вам хочется завершить работу исполнителя без ожидания, используйте вместо этого метод shutdownNow()
.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyRunnable implements Runnable {
private final String task;
MyRunnable(String task) {
this.task = task;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("Executing "+ task +" with "+Thread.currentThread().getName());
}
System.out.println();
}
}
public class Exec2 {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 1; i <= 5; i++) {
Runnable worker = new MyRunnable("Task" + i);
executor.execute(worker);
}
executor.shutdown();
}
}
Использован тот же пример, что и в предыдущем случае, только на этот раз — с методом newFixedThreadPool()
. Этот метод позволяет создать пул с фиксированным количеством потоков. Таким образом, когда мы отправим пять задач, в коде будет создано три новых потока и будут выполнены три задачи. Остальные две задачи находятся в очереди ожидания. Как только какая-либо задача выполнится потоком, этим же потоком будет выбрана и выполнена следующая задача.
Когда мы создаем пул потоков с помощью этого метода, максимальный размер пула потоков устанавливается на максимальное целочисленное значение в Java. Этот метод создает новые потоки по запросу и разрушает потоки, которые простаивают больше минуты, если запрос отсутствует.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyRunnable implements Runnable {
private final String task;
MyRunnable(String task) {
this.task = task;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("Executing "+ task +" with "+Thread.currentThread().getName());
}
System.out.println();
}
}
public class Exec3 {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 1; i <= 5; i++) {
Runnable worker = new MyRunnable("Task" + i);
executor.execute(worker);
}
executor.shutdown();
}
}
В данном примере метод newCachedThreadPool()
изначально создаст пять новых потоков и обработает пять задач. Никакой очереди ожидания здесь не будет. Если поток остается в бездействии более минуты, метод устраняет его. Таким образом, этот метод — хороший выбор, если вам хочется добиться большей производительности очереди, чем это возможно с методом newFixedThreadPool()
. Но если вы хотите ограничить количество параллельно выполняемых задач во имя управления ресурсами, лучше использовать newFixedThreadPool()
.
Метод newScheduledThreadPool()
создает пул потоков, который может планировать выполнение задач после заданной задержки или через регулярные промежутки времени. Этот метод возвращает ScheduledExecutorService
. Существует три метода для планирования задач в таком пуле потоков: schedule()
, scheduleAtFixedRate()
и scheduleWithFixedDelay()
. Рассмотрим пример реализации пула потоков с помощью метода schedule()
.
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
class RunnableTask implements Runnable{
private String task;
public RunnableTask(String s){
this.task = s;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" Start Time for "+task+" "+new Date());
// Обрабатываем задачу здесь
try {
Thread.sleep(3000); // обработка задачи занимает три секунды
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" End Time for "+task+" "+new Date());
}
@Override
public String toString(){
return this.task;
}
}
public class ScheduledThreadPoolMain {
public static void main(String[] args) throws InterruptedException {
System.out.println("Current Time = "+new Date());
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
for(int i=1; i<=3; i++){
Thread.sleep(2000);
RunnableTask task = new RunnableTask("Task "+i);
scheduledThreadPool.schedule(task,3, TimeUnit.SECONDS); // планируется задержка выполнения задачи на три секунды
}
Thread.sleep(6000); // добавляем некоторую задержку
scheduledThreadPool.shutdown();
System.out.println("Completed all threads");
}
}
Как видно из примера:
schedule
принимает три аргумента: задачу, задержку и промежуток времени задержки. schedule()
используется для планирования задачи после фиксированной задержки. scheduleAtFixedRate()
используется для планирования задачи после фиксированной задержки и последующего периодического выполнения этой задачи. scheduleWithFixedDelay()
используется для планирования задачи после начальной задержки, а затем выполнения задач с фиксированной задержкой после завершения предыдущей задачи.Каждый из этих методов полезен в своём определённом сценариев.
Вот и всё о пулах потоков. Теперь — некоторые важные замечания насчет того, как использовать фреймворк Executor.
shutdown()
. Если этого не сделать, программа будет продолжать работать без конца. Если вы отправите исполнителю другую задачу после завершения работы, она выдаст исключение RejectedExecutionException
.На этом всё. Надеюсь, пулы потоков и Java ExecutorService стали для вас понятнее. Спасибо за чтение и счастливого кодирования!
Перевод статьи Manusha Chethiyawardhana: “How to use the Executor Framework in Java”
Комментарии