Hur man startar en tråd i Java

1. Introduktion

I den här handledningen ska vi utforska olika sätt att starta en tråd och utföra parallella uppgifter.

Detta är mycket användbart, särskilt när man hanterar långa eller återkommande operationer som inte kan köras på huvudtråden , eller där användargränssnittsinteraktionen inte kan läggas i väntan på operationens resultat.

För att lära dig mer om trådarnas detaljer, läs definitivt vår handledning om en tråds livscykel i Java.

2. Grunderna för att köra en tråd

Vi kan enkelt skriva lite logik som går i en parallell tråd genom att använda trådramen .

Låt oss prova ett grundläggande exempel genom att utöka klassen tråd :

public class NewThread extends Thread { public void run() { long startTime = System.currentTimeMillis(); int i = 0; while (true) { System.out.println(this.getName() + ": New Thread is running..." + i++); try { //Wait for one sec so it doesn't print too fast Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } ... } } }

Och nu skriver vi en andra klass för att initialisera och starta vår tråd:

public class SingleThreadExample { public static void main(String[] args) { NewThread t = new NewThread(); t.start(); } }

Vi bör anropa start () -metoden på trådar i NEW- tillståndet (motsvarigheten till inte startad). I annat fall kastar Java en instans av IllegalThreadStateException- undantag.

Låt oss nu anta att vi måste starta flera trådar:

public class MultipleThreadsExample { public static void main(String[] args) { NewThread t1 = new NewThread(); t1.setName("MyThread-1"); NewThread t2 = new NewThread(); t2.setName("MyThread-2"); t1.start(); t2.start(); } }

Vår kod ser fortfarande ganska enkel ut och mycket lik de exempel vi kan hitta online.

Naturligtvis är detta långt ifrån produktionsklar kod, där det är av avgörande betydelse att hantera resurser på rätt sätt, för att undvika för mycket kontextbyte eller för mycket minnesanvändning.

Så för att bli produktionsklar måste vi nu skriva ytterligare pannplatta för att hantera:

  • konsekvent skapandet av nya trådar
  • antalet samtidiga live-trådar
  • threads deallocation: mycket viktigt för daemontrådar för att undvika läckor

Om vi ​​vill kan vi skriva vår egen kod för alla dessa fallscenarier och till och med några fler, men varför ska vi uppfinna hjulet igen?

3. ExecutorService Framework

De ExecutorService implementerar Thread Pool designmönster (även kallad en replikerad arbetare eller arbetar crew modell) och tar hand om gäng management vi nämnde ovan, plus det tillför några mycket användbara funktioner som gängåteranvändbarhet och uppgiftsköer.

I synnerhet trådåteranvändbarhet är mycket viktigt: i en storskalig applikation skapar en allomfattande minneshanteringskostnad allokering och omlokalisering av många trådobjekt.

Med arbetartrådar minimerar vi omkostnaderna som skapas av trådar.

För att underlätta poolkonfigurationen kommer ExecutorService med en enkel konstruktör och några anpassningsalternativ, till exempel typ av kö, minimi- och maximalt antal trådar och deras namnkonvention.

För mer information om ExecutorService, läs vår guide till Java ExecutorService.

4. Starta en uppgift med exekutörer

Tack vare detta kraftfulla ramverk kan vi ändra tankesättet från att starta trådar till att skicka in uppgifter.

Låt oss titta på hur vi kan skicka en asynkron uppgift till vår exekutor:

ExecutorService executor = Executors.newFixedThreadPool(10); ... executor.submit(() -> { new Task(); });

Det finns två metoder vi kan använda: exekvera , som inte returnerar något, och skicka , som returnerar en framtid som inkapslar beräkningsresultatet.

För mer information om Futures, läs vår guide till java.util.concurrent.Future.

5. Starta en uppgift med CompletableFutures

För att hämta det slutliga resultatet från ett Future- objekt kan vi använda get- metoden som är tillgänglig i objektet, men detta skulle blockera modertråden till slutet av beräkningen.

Alternativt kan vi undvika blocket genom att lägga till mer logik i vår uppgift, men vi måste öka komplexiteten i vår kod.

Java 1.8 introducerade ett nytt ramverk ovanpå Future- konstruktionen för att bättre arbeta med beräkningsresultatet: CompletableFuture .

CompletableFuture implementerar CompletableStage , som lägger till ett stort urval av metoder för att bifoga återuppringningar och undvika all VVS som behövs för att köra operationer på resultatet när det är klart.

Implementeringen för att skicka en uppgift är mycket enklare:

CompletableFuture.supplyAsync(() -> "Hello");

supplyAsync tar en leverantör som innehåller koden som vi vill utföra asynkront - i vårt fall lambda-parametern.

Uppgiften skickas nu implicit till ForkJoinPool.commonPool () , eller så kan vi ange den exekutör som vi föredrar som en andra parameter.

Om du vill veta mer om CompletableFuture, läs vår guide till CompletableFuture.

6. Köra försenade eller periodiska uppgifter

När vi arbetar med komplexa webbapplikationer kan vi behöva köra uppgifter vid specifika tider, kanske regelbundet.

Java har få verktyg som kan hjälpa oss att köra försenade eller återkommande operationer:

  • java.util.Timer
  • java.util.concurrent.ScheduledThreadPoolExecutor

6.1. Timer

Timer är en möjlighet att schemalägga uppgifter för framtida körning i en bakgrundstråd.

Uppgifter kan schemaläggas för engångskörning eller för upprepad körning med jämna mellanrum.

Låt oss se hur koden ser ut om vi vill köra en uppgift efter en sekund fördröjning:

TimerTask task = new TimerTask() { public void run() { System.out.println("Task performed on: " + new Date() + "n" + "Thread's name: " + Thread.currentThread().getName()); } }; Timer timer = new Timer("Timer"); long delay = 1000L; timer.schedule(task, delay);

Låt oss nu lägga till ett återkommande schema:

timer.scheduleAtFixedRate(repeatedTask, delay, period);

Den här gången kommer uppgiften att köras efter den angivna fördröjningen och den kommer att återkomma efter den tidsperiod som gått.

För mer information, läs vår guide till Java Timer.

6.2. ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor has methods similar to the Timer class:

ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2); ScheduledFuture resultFuture = executorService.schedule(callableTask, 1, TimeUnit.SECONDS);

To end our example, we use scheduleAtFixedRate() for recurring tasks:

ScheduledFuture resultFuture = executorService.scheduleAtFixedRate(runnableTask, 100, 450, TimeUnit.MILLISECONDS);

The code above will execute a task after an initial delay of 100 milliseconds, and after that, it'll execute the same task every 450 milliseconds.

If the processor can't finish processing the task in time before the next occurrence, the ScheduledExecutorService will wait until the current task is completed, before starting the next.

To avoid this waiting time, we can use scheduleWithFixedDelay(), which, as described by its name, guarantees a fixed length delay between iterations of the task.

För mer information om ScheduledExecutorService, läs vår guide till Java ExecutorService.

6.3. Vilket verktyg är bättre?

Om vi ​​kör exemplen ovan ser beräkningsresultatet detsamma ut.

Så, hur väljer vi rätt verktyg ?

När ett ramverk erbjuder flera val är det viktigt att förstå den underliggande tekniken för att fatta ett välgrundat beslut.

Låt oss försöka dyka lite djupare under huven.

Timer :

  • erbjuder inte garantier i realtid: den schemalägger uppgifter med Object.wait (lång) -metoden
  • det finns en enda bakgrundstråd, så uppgifter körs sekventiellt och en långvarig uppgift kan fördröja andra
  • runtime exceptions thrown in a TimerTask would kill the only thread available, thus killing Timer

ScheduledThreadPoolExecutor:

  • can be configured with any number of threads
  • can take advantage of all available CPU cores
  • catches runtime exceptions and lets us handle them if we want to (by overriding afterExecute method from ThreadPoolExecutor)
  • cancels the task that threw the exception, while letting others continue to run
  • relies on the OS scheduling system to keep track of time zones, delays, solar time, etc.
  • provides collaborative API if we need coordination between multiple tasks, like waiting for the completion of all tasks submitted
  • provides better API for management of the thread life cycle

The choice now is obvious, right?

7. Difference Between Future and ScheduledFuture

In our code examples, we can observe that ScheduledThreadPoolExecutor returns a specific type of Future: ScheduledFuture.

ScheduledFuture extends both Future and Delayed interfaces, thus inheriting the additional method getDelay that returns the remaining delay associated with the current task. It's extended by RunnableScheduledFuture that adds a method to check if the task is periodic.

ScheduledThreadPoolExecutor implementerar alla dessa konstruktioner genom den inre klassen ScheduledFutureTask och använder dem för att styra uppgiftslivscykeln.

8. Slutsatser

I denna handledning experimenterade vi med de olika ramarna som var tillgängliga för att starta trådar och köra uppgifter parallellt.

Sedan gick vi djupare in i skillnaderna mellan Timer och ScheduledThreadPoolExecutor.

Källkoden för artikeln finns tillgänglig på GitHub.