Varför behöver du Executor-gränssnittet?

Före Java 5 var du tvungen att skriva all din egen kodtrådshantering i din applikation. Dessutom skapar enny trådobjekt är en resurskrävande operation, och det är inte meningsfullt att skapa en ny tråd för varje lätt uppgift. Och eftersom detta problem är bekant för absolut alla utvecklare av flertrådade applikationer, bestämde de sig för att ta med denna funktionalitet i Java som Executor -ramverket.

Vad är den stora idén? Det är enkelt: istället för att skapa en ny tråd för varje ny uppgift, förvaras trådar i ett slags "lager", och när en ny uppgift kommer, hämtar vi en befintlig tråd istället för att skapa en ny.

Huvudgränssnitten i detta ramverk är Executor , ExecutorService och ScheduledExecutorService , som var och en utökar funktionaliteten hos den tidigare.

Executor-gränssnittet är basgränssnittet. Den deklarerar en enda void execute (Körbart kommando) metod som implementeras av ett Körbart objekt.

ExecutorService - gränssnittet är mer intressant. Den har metoder för att hantera slutförandet av arbete, samt metoder för att returnera något slags resultat. Låt oss ta en närmare titt på dess metoder:

Metod Beskrivning
void shutdown(); Att anropa den här metoden stoppar ExecutorService . Alla uppgifter som redan har lämnats in för behandling kommer att slutföras, men nya uppgifter accepteras inte.
List<Runnable> shutdownNow();

Att anropa den här metoden stoppar ExecutorService . Thread.interrupt kommer att anropas för alla uppgifter som redan har lämnats in för bearbetning. Den här metoden returnerar en lista över köade uppgifter.

Metoden väntar inte på slutförandet av alla uppgifter som är "pågående" när metoden anropas.

Varning: Att anropa den här metoden kan läcka resurser.

boolean isShutdown(); Kontrollerar om ExecutorService är stoppad.
boolean isTerminated(); Returnerar sant om alla uppgifter slutfördes efter avstängning av ExecutorService . Tills shutdown() eller shutdownNow() anropas kommer den alltid att returnera false .
boolean awaitTermination (lång timeout, TimeUnit-enhet) kastar InterruptedException;

Efter att metoden shutdown() har anropats, blockerar denna metod tråden som den anropas på, tills något av följande villkor är sant:

  • alla schemalagda uppgifter är klara;
  • timeouten som överförts till metoden har förflutit;
  • den aktuella tråden avbryts.

Returnerar sant om alla uppgifter är slutförda, och falskt om timeouten löper ut före avslutning.

<T> Future<T> submit(Callable<T> task);

Lägger till en Callable -uppgift till ExecutorService och returnerar ett objekt som implementerar Future- gränssnittet.

<T> är typen av resultatet av den godkända uppgiften.

<T> Future<T> submit(Körbar uppgift, T-resultat);

Lägger till en körbar uppgift till ExecutorService och returnerar ett objekt som implementerar Future- gränssnittet.

T -resultatparametern är det som returneras av ett anrop till get()- metoden på resultatetFramtida objekt.

Future<?> submit(Körbar uppgift);

Lägger till en körbar uppgift till ExecutorService och returnerar ett objekt som implementerar Future- gränssnittet.

Om vi ​​anropar metoden get() på det resulterande Future- objektet får vi null.

<T> List<Future<T>> invokeAll(Collection<? utökar Callable<T>> uppgifter) kastar InterruptedException;

Skickar en lista över anropsbara uppgifter till ExecutorService . Returnerar en lista över Futures från vilka vi kan få resultatet av arbetet. Denna lista returneras när alla inskickade uppgifter är klara.

Om uppgiftssamlingen ändras medan metoden körs är resultatet av denna metod odefinierat.

<T> List<Future<T>> invokeAll(Collection<? utökar Callable<T>>-uppgifter, lång timeout, TimeUnit-enhet) kastar InterruptedException;

Skickar en lista över anropsbara uppgifter till ExecutorService . Returnerar en lista över Futures från vilka vi kan få resultatet av arbetet. Den här listan returneras när alla godkända uppgifter har slutförts, eller efter att timeouten som skickats till metoden har löpt ut, beroende på vilket som kommer först.

Om timeouten går ut avbryts oavslutade uppgifter.

Obs: Det är möjligt att en avbruten uppgift inte slutar köra (vi kommer att se denna bieffekt i exemplet).

Om uppgiftssamlingen ändras medan metoden körs är resultatet av denna metod odefinierat.

<T> T invokeAny(Collection<? utökar Callable<T>> uppgifter) kastar InterruptedException, ExecutionException;

Skickar en lista över anropsbara uppgifter till ExecutorService . Returnerar resultatet av en av uppgifterna (om någon) som slutfördes utan att göra ett undantag (om något).

Om uppgiftssamlingen ändras medan metoden körs är resultatet av denna metod odefinierat.

<T> T invokeAny(Collection<? utökar Callable<T>>-uppgifter, lång timeout, TimeUnit-enhet) kastar InterruptedException, ExecutionException, TimeoutException;

Skickar en lista över anropsbara uppgifter till ExecutorService . Returnerar resultatet av en av uppgifterna (om någon) som slutfördes utan att göra ett undantag innan den timeout som skickats till metoden har löpt ut.

Om uppgiftssamlingen ändras medan metoden körs är resultatet av denna metod odefinierat.

Låt oss titta på ett litet exempel på att arbeta med 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;
       }
   }
}

Produktion:

gjort
klar
Futures mottagen
sömn 1: sömn avbruten
sömn 1: sömn avbruten
gjort
gjort
sant
sant

Varje uppgift pågår i 5 sekunder. Vi skapade en pool för två trådar, så de två första utdataraderna är helt logiska.

Sex sekunder efter att programmet startar, tar invokeAll- metoden timeout och resultatet returneras som en lista över Futures . Detta kan ses från utgångssträngen Futures mottagna .

När de två första uppgifterna är gjorda börjar ytterligare två. Men eftersom timeouten som ställts in i invokeAll -metoden löper ut, har dessa två uppgifter inte tid att slutföra. De får ett "avbryt" -kommando. Det är därför utgången har två rader med viloläge 1: viloläge avbruten .

Och sedan kan du se ytterligare två rader med klar . Detta är bieffekten som jag nämnde när jag beskrev invokeAll -metoden.

Den femte och sista uppgiften kommer aldrig ens igång, så vi ser inget om det i utgången.

De två sista raderna är resultatet av anropet av metoderna isShutdown och isTerminated .

Det är också intressant att köra det här exemplet i felsökningsläge och titta på aktivitetens status efter att timeout har gått (ställ in en brytpunkt på raden med executorService.shutdown(); ):

Vi ser att två uppgifter slutfördes normalt och tre uppgifter var "Avbrutna" .

ScheduledExecutorService

För att avsluta vår diskussion om exekutörer, låt oss ta en titt på ScheduledExecutorService .

Den har 4 metoder:

Metod Beskrivning
public ScheduledFuture<?>-schema(Körbart kommando, lång fördröjning, TimeUnit-enhet); Schemalägger den godkända körbara uppgiften att köras en gång efter fördröjningen som anges som argument.
public <V> ScheduledFuture<V>-schema(Callable<V> callable, long delay, TimeUnit unit); Schemalägger den godkända Callable -uppgiften att köras en gång efter fördröjningen som anges som ett argument.
public ScheduledFuture<?> scheduleAtFixedRate(Körbart kommando, long initialDelay, long period, TimeUnit unit); Schemalägger periodisk exekvering av den godkända uppgiften, som kommer att köras för första gången efter initialDelay , och varje efterföljande körning börjar efter period .
public ScheduledFuture<?> scheduleWithFixedDelay(Körbart kommando, lång initialDelay, lång fördröjning, TimeUnit-enhet); Schemalägger periodisk exekvering av den godkända uppgiften, som kommer att exekveras för första gången efter initialDelay , och varje efterföljande körning börjar efter fördröjning (perioden mellan slutförandet av föregående körning och början av den aktuella).