Защо ви е необходим интерфейсът на Executor?

Преди Java 5 трябваше да напишете цялото си собствено управление на codeови нишки във вашето приложение. В допълнение, създаването на aнова темаобект е ресурсоемка операция и няма смисъл да се създава нова нишка за всяка лека задача. И тъй като този проблем е познат на абсолютно всеки разработчик на многонишкови applications, те решиха да внедрят тази функционалност в Java като Executor framework.

Каква е голямата идея? Това е просто: instead of да се създава нова нишка за всяка нова задача, нишките се съхраняват в един вид „хранorще“ и когато пристигне нова задача, ние извличаме съществуваща нишка, instead of да създаваме нова.

Основните интерфейси на тази рамка са Executor , ExecutorService и ScheduledExecutorService , всеки от които разширява функционалността на предишния.

Интерфейсът Executor е основният интерфейс. Той декларира единичен метод на void execute (Runnable command) , който е имплементиран от Runnable обект.

Интерфейсът ExecutorService е по-интересен. Има методи за управление на завършването на работата, Howто и методи за връщане на няHowъв резултат. Нека разгледаме по-отблизо неговите методи:

Метод Описание
void shutdown(); Извикването на този метод спира ExecutorService . Всички задачи, които вече са изпратени за обработка, ще бъдат изпълнени, но нови задачи няма да бъдат приети.
Списък<Runnable> shutdownNow();

Извикването на този метод спира ExecutorService . Thread.interrupt ще бъде извикан за всички задачи, които вече са изпратени за обработка. Този метод връща списък със задачи в опашка.

Методът не изчаква завършването на всички задачи, които са "в процес" в момента на извикване на метода.

Предупреждение: Извикването на този метод може да доведе до изтичане на ресурси.

булево isShutdown(); Проверява дали ExecutorService е спрян.
boolean isTerminated(); Връща true, ако всички задачи са изпълнени след изключване на ExecutorService . Докато shutdown() or shutdownNow() не бъдат извикани, те винаги ще връщат false .
boolean awaitTermination(дълго изчакване, единица TimeUnit) хвърля InterruptedException;

След като методът shutdown() бъде извикан, този метод блокира нишката, на която е извикан, докато не е изпълнено едно от следните условия:

  • всички планирани задачи са изпълнени;
  • времето за изчакване, предадено на метода, е изтекло;
  • текущата нишка е прекъсната.

Връща true , ако всички задачи са изпълнени, и false , ако времето за изчакване изтече преди прекратяването.

<T> Бъдещо<T> подаване (Callable<T> задача);

Добавя Callable задача към ExecutorService и връща обект, който имплементира интерфейса Future .

<T> е типът на резултата от предадената задача.

<T> Бъдещо<T> подаване (изпълнима задача, T резултат);

Добавя Runnable задача към ExecutorService и връща обект, който имплементира интерфейса Future .

Параметърът T result е това, което се връща от извикване на метода get() на резултатаБъдещ обект.

Бъдещо<?> подаване (изпълнима задача);

Добавя Runnable задача към ExecutorService и връща обект, който имплементира интерфейса Future .

Ако извикаме метода get() на резултантния Future обект, тогава получаваме null.

<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) хвърля InterruptedException;

Подава списък с извикваеми задачи към ExecutorService . Връща списък с фючърси, от които можем да получим резултата от работата. Този списък се връща, когато всички изпратени задачи са изпълнени.

Ако колекцията от задачи е променена, докато методът се изпълнява, резултатът от този метод е недефиниран.

<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) хвърля InterruptedException;

Подава списък с извикваеми задачи към ExecutorService . Връща списък с фючърси, от които можем да получим резултата от работата. Този списък се връща, когато всички преминати задачи са завършени or след изтичане на времето за изчакване, предадено на метода, което от двете настъпи първо.

Ако времето за изчакване изтече, незавършените задачи се анулират.

Забележка: Възможно е отменена задача да не спре да се изпълнява (ще видим този страничен ефект в примера).

Ако колекцията от задачи е променена, докато методът се изпълнява, резултатът от този метод е недефиниран.

<T> T invokeAny(Collection<? extends Callable<T>> tasks) хвърля InterruptedException, ExecutionException;

Подава списък с извикваеми задачи към ExecutorService . Връща резултата от една от задачите (ако има такива), които са завършor без хвърляне на изключение (ако има такова).

Ако колекцията от задачи е променена, докато методът се изпълнява, резултатът от този метод е недефиниран.

<T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) хвърля InterruptedException, ExecutionException, TimeoutException;

Подава списък с извикваеми задачи към ExecutorService . Връща резултата от една от задачите (ако има такива), която е завършила без хвърляне на изключение преди времето за изчакване, предадено на метода, да е изтекло.

Ако колекцията от задачи е променена, докато методът се изпълнява, резултатът от този метод е недефиниран.

Нека да разгледаме малък пример за работа с ExecutorService .


import java.util.List;
import java.util.concurrent.*;

public class ExecutorServiceTest {
   public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {
//Create an ExecutorService for 2 threads
       java.util.concurrent.ExecutorService executorService = new ThreadPoolExecutor(2, 2, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
// Create 5 tasks
       MyRunnable task1 = new MyRunnable();
       MyRunnable task2 = new MyRunnable();
       MyRunnable task3 = new MyRunnable();
       MyRunnable task4 = new MyRunnable();
       MyRunnable task5 = new MyRunnable();

       final List<MyRunnable> tasks = List.of(task1, task2, task3, task4, task5);
// Pass a list that contains the 5 tasks we created
       final List<Future<Void>> futures = executorService.invokeAll(tasks, 6, TimeUnit.SECONDS);
       System.out.println("Futures received");

// Stop the ExecutorService
       executorService.shutdown();

       try {
           TimeUnit.SECONDS.sleep(3);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }

       System.out.println(executorService.isShutdown());
       System.out.println(executorService.isTerminated());
   }

   public static class MyRunnable implements Callable<Void> {

       @Override
       public void call() {
// Add 2 delays. When the ExecutorService is stopped, we will see which delay is in progress when the attempt is made to stop execution of the task
           try {
               TimeUnit.SECONDS.sleep(3);
           } catch (InterruptedException e) {
               System.out.println("sleep 1: " + e.getMessage());
           }
           try {
               TimeUnit.SECONDS.sleep(2);
           } catch (InterruptedException e) {
               System.out.println("sleep 2: " + e.getMessage());
           }
           System.out.println("done");
           return null;
       }
   }
}

Изход:

готово
напequalsо
получени фючърси сън 1 :
сън прекъснат
сън 1: сън прекъснат
напequalsо вярно вярно


Всяка задача се изпълнява за 5 секунди. Създадохме пул за две нишки, така че първите два изходни реда имат идеален смисъл.

Шест секунди след стартиране на програмата, времето за изчакване на метода invokeAll изтича и резултатът се връща като списък с фючърси . Това може да се види от изходния низ Получени фючърси .

След изпълнението на първите две задачи започват още две. Но тъй като времето за изчакване, зададено в метода invokeAll , изтича, тези две задачи нямат време за изпълнение. Те получават команда "отказ" . Ето защо изходът има два реда със заспиване 1: заспиване прекъснато .

След това можете да видите още два реда с done . Това е страничният ефект, който споменах, когато описах метода invokeAll .

Петата и последна задача дори не започва, така че не виждаме нищо за нея в изхода.

Последните два реда са резултат от извикването на методите isShutdown и isTerminated .

Също така е интересно да стартирате този пример в режим на отстраняване на грешки и да разгледате състоянието на задачата след изтичане на времето за изчакване (задайте точка на прекъсване на реда с executorService.shutdown(); ):

Виждаме, че две задачи са изпълнени нормално и три задачи са „Отменени“ .

ScheduledExecutorService

За да завършим нашата дискусия за изпълнителите, нека да разгледаме ScheduledExecutorService .

Има 4 метода:

Метод Описание
публичен график ScheduledFuture<?> (изпълнима команда, дълго забавяне, единица TimeUnit); Планира подадената Runnable задача да се изпълни веднъж след забавянето, посочено като аргумент.
публичен <V> ScheduledFuture<V> график (Callable<V> callable, дълго забавяне, TimeUnit единица); Планира предадената Callable задача да се изпълни веднъж след закъснението, посочено като аргумент.
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit); Планира периодично изпълнение на предадената задача, която ще бъде изпълнена за първи път след initialDelay и всяко следващо изпълнение ще започне след period .
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit); Планира периодично изпълнение на предадената задача, която ще бъде изпълнена за първи път след initialDelay и всяко следващо изпълнение ще започне след забавяне (периода между завършването на предишното изпълнение и началото на текущото).