9 fejl, du skal undgå, når du programmerer i Java
Hvilke fejl bør man undgå, når man programmerer i Java? I det følgende afsnit besvarer vi dette spørgsmål.
Læs første del af vores blogserie om samtidighed i Java. I den følgende artikel vil vi se nærmere på forskellene mellem tråd og proces, trådpuljer, eksekutorer og meget mere!
Generelt er den konventionelle programmeringstilgang sekventiel. Alt i et program sker et trin ad gangen.
Men faktisk er det parallelle den måde, hele verden kører på - det er evnen til at udføre mere end én opgave samtidig.
At diskutere avancerede emner som samtidighed i Java eller multithreading, er vi nødt til at aftale nogle fælles definitioner for at være sikre på, at vi er på samme side.
Lad os starte med det grundlæggende. I den ikke-sekventielle verden har vi to slags samtidighedsrepræsentanter: processer og
tråde. En proces er en instans af det program, der kører. Normalt er den isoleret fra andre processer.
Operativsystemet er ansvarligt for at tildele ressourcer til hver proces. Desuden fungerer det som en leder, der
planlægger og kontrollerer dem.
En tråd er en slags proces, men på et lavere niveau, og derfor kaldes den også en let tråd. Flere tråde kan køre i en
proces. Her fungerer programmet som en scheduler og en controller for tråde. På denne måde ser de enkelte programmer ud til at gøre
flere opgaver på samme tid.
Den grundlæggende forskel mellem tråde og processer er isolationsniveauet. Processen har sit eget sæt af
ressourcer, mens tråden deler data med andre tråde. Det kan virke som en fejlbehæftet tilgang, og det er det også. For
Lad os nu ikke fokusere på det, da det ligger uden for rammerne af denne artikel.
Processer, tråde - ok ... Men hvad er egentlig samtidighed? Samtidighed betyder, at du kan udføre flere opgaver på samme tid.
tid. Det betyder ikke, at de opgaver skal køre samtidig - det er det, der er parallelisme. Concurrenc i Javay gør det heller ikke
kræver, at du har flere CPU'er eller endda flere kerner. Det kan opnås i et miljø med én kerne ved at udnytte
skift af kontekst.
Et begreb, der er relateret til samtidighed, er multithreading. Det er en funktion i programmer, der gør det muligt for dem at udføre flere opgaver på én gang. Ikke alle programmer bruger denne tilgang, men dem, der gør, kan kaldes flertrådede.
Vi er næsten klar til at gå i gang, bare en definition mere. Asynkroni betyder, at et program udfører ikke-blokerende operationer.
Den påbegynder en opgave og fortsætter derefter med andre ting, mens den venter på responsen. Når den får svaret, kan den reagere på det.
Som standard vil hver Java-applikation kører i én proces. I den proces er der en tråd, der er relateret til main()
metode til
en applikation. Men som nævnt er det muligt at udnytte mekanismerne i flere tråde inden for en
program.
Tråd
er en Java klasse, hvor magien sker. Dette er objektrepræsentationen af den førnævnte tråd. Til
oprette din egen tråd, kan du udvide Tråd
klasse. Det er dog ikke en anbefalet tilgang. Tråde
skal bruges som en mekanisme, der kører opgaven. Opgaver er dele af Kode som vi ønsker at køre i en samtidig tilstand. Vi kan definere dem ved hjælp af Kan køres
interface.
Men nok teori, lad os bruge vores kode, hvor vores mund er.
Lad os antage, at vi har et par arrays af tal. For hver matrix vil vi gerne vide, hvad summen af tallene i en matrix er. Lad os
Lad som om der er mange af den slags arrays, og hver af dem er relativt store. Under sådanne forhold vil vi gøre brug af samtidighed og summere hvert array som en separat opgave.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};
Runnable task1 = () -> {
int sum = Arrays.stream(a1).sum();
System.out.println("1. Summen er: " + sum);
};
Runnable task2 = () -> {
int sum = Arrays.stream(a2).sum();
System.out.println("2. Summen er: " + sum);
};
Runnable task3 = () -> {
int sum = Arrays.stream(a3).sum();
System.out.println("3. Summen er: " + sum);
};
new Thread(task1).start();
new Thread(task2).start();
new Thread(task3).start();
Som du kan se i koden ovenfor Kan køres
er en funktionel grænseflade. Den indeholder en enkelt abstrakt metode kør()
uden argumenter. Den Kan køres
interface bør implementeres af enhver klasse, hvis instanser er beregnet til at være
udført af en tråd.
Når du har defineret en opgave, kan du oprette en tråd til at køre den. Dette kan gøres via ny tråd()
konstruktør, der
tager Kan køres
som sit argument.
Det sidste skridt er at start()
en nyoprettet tråd. I API'en er der også kør()
metoder i Kan køres
og iTråd
. Men det er ikke en måde at udnytte samtidighed på i Java. Et direkte kald til hver af disse metoder resulterer i
udføre opgaven i den samme tråd som main()
metoden kører.
Når der er mange opgaver, er det ikke en god idé at oprette en separat tråd for hver enkelt. Oprettelse af en Tråd
er en
tung operation, og det er langt bedre at genbruge eksisterende tråde end at oprette nye.
Når et program opretter mange kortvarige tråde, er det bedre at bruge en trådpulje. Trådpuljen indeholder et antal
klar til at køre, men i øjeblikket ikke aktive tråde. At give en Kan køres
til puljen får en af trådene til at kaldekør()
metode af givet Kan køres
. Når en opgave er afsluttet, eksisterer tråden stadig og er i en inaktiv tilstand.
Okay, du har forstået det - du foretrækker trådpuljer i stedet for manuel oprettelse. Men hvordan kan du gøre brug af trådpuljer? Den Eksekutorer
Klassen har en række statiske fabriksmetoder til konstruktion af trådpuljer. For eksempel newCachedThredPool()
skaber
en pulje, hvor nye tråde oprettes efter behov, og inaktive tråde opbevares i 60 sekunder. I modsætning hertil,newFixedThreadPool()
indeholder et fast sæt af tråde, hvor inaktive tråde opbevares på ubestemt tid.
Lad os se, hvordan det kan fungere i vores eksempel. Nu behøver vi ikke at oprette tråde manuelt. I stedet skal vi opretteUdførerService
som giver en pulje af tråde. Derefter kan vi tildele opgaver til den. Det sidste trin er at lukke tråden
pool for at undgå hukommelseslækager. Resten af den tidligere kode forbliver den samme.
ExecutorService executor = Executors.newCachedThreadPool();
executor.submit(opgave1);
executor.submit(opgave2);
executor.submit(opgave3);
executor.shutdown();
Kan køres
virker som en smart måde at skabe samtidige opgaver på, men den har en stor mangel. Den kan ikke returnere nogen
værdi. Desuden kan vi ikke afgøre, om en opgave er færdig eller ej. Vi ved heller ikke, om den er afsluttet.
normalt eller usædvanligt. Løsningen på disse problemer er Kan kaldes
.
Kan kaldes
svarer til Kan køres
På en måde omslutter den også asynkrone opgaver. Den største forskel er, at den er i stand til at
returnere en værdi. Returværdien kan være af en hvilken som helst (ikke-primitiv) type, da Kan kaldes
interface er en parameteriseret type.Kan kaldes
er en funktionel grænseflade, der har opkald()
metode, som kan kaste en Undtagelse
.
Lad os nu se, hvordan vi kan udnytte Kan kaldes
i vores array-problem.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};
Callable task1 = () -> Arrays.stream(a1).sum();
Callable task2 = () -> Arrays.stream(a2).sum();
Callable task3 = () -> Arrays.stream(a3).sum();
ExecutorService executor = Executors.newCachedThreadPool();
Future future1 = executor.submit(task1);
Future future2 = executor.submit(task2);
Future future3 = executor.submit(task3);
System.out.println("1. Summen er: " + future1.get());
System.out.println("2. Summen er: " + future2.get());
System.out.println("3. Summen er: " + future3.get());
executor.shutdown();
Okay, vi kan se, hvordan Kan kaldes
oprettes og sendes derefter til UdførerService
. Men hvad pokker er Fremtiden
?Fremtiden
fungerer som en bro mellem trådene. Summen af hvert array produceres i en separat tråd, og vi har brug for en måde at
få disse resultater tilbage til main()
.
For at hente resultatet fra Fremtiden
Vi er nødt til at ringe get()
metode. Her kan der ske en af to ting. For det første kan
resultatet af den beregning, der udføres af Kan kaldes
er tilgængelig. Så får vi det med det samme. For det andet er resultatet ikke
klar endnu. I det tilfælde get()
metoden vil blokere, indtil resultatet er tilgængeligt.
Problemet med Fremtiden
er, at det fungerer i 'push-paradigmet'. Når man bruger Fremtiden
Du skal være som en chef, der
spørger konstant: "Er din opgave færdig? Er den klar?", indtil den giver et resultat. At handle under konstant pres er
dyrt. En langt bedre tilgang ville være at bestille Fremtiden
hvad den skal gøre, når den er klar med sin opgave. Desværre,Fremtiden
kan ikke gøre det, men ComputableFuture
kan.
ComputableFuture
arbejder i 'pull-paradigmet'. Vi kan fortælle den, hvad den skal gøre med resultatet, når den er færdig med sine opgaver. Den
er et eksempel på en asynkron tilgang.
ComputableFuture
fungerer perfekt med Kan køres
men ikke med Kan kaldes
. I stedet er det muligt at levere en opgave tilComputableFuture
i form af Leverandør
.
Lad os se, hvordan ovenstående hænger sammen med vores problem.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};
CompletableFuture.supplyAsync(() -> Arrays.stream(a1).sum())
.thenAccept(System.out::println);
CompletableFuture.supplyAsync(() -> Arrays.stream(a2).sum())
.thenAccept(System.out::println);
CompletableFuture.supplyAsync(() -> Arrays.stream(a3).sum())
.thenAccept(System.out::println);
Det første, der slår dig, er, hvor meget kortere denne løsning er. Desuden ser det også pænt og ryddeligt ud.
Opgave til FuldstændigFremtid
kan leveres af supplyAsync()
metode, der tager Leverandør
eller af runAsync()
at
tager Kan køres
. Et tilbagekald - et stykke kode, der skal køres, når opgaven er fuldført - defineres af thenAccept()
metode.
Java giver mange forskellige tilgange til samtidighed. I denne artikel har vi kun lige berørt emnet.
Ikke desto mindre dækkede vi det grundlæggende i Tråd
, Kan køres
, Kan kaldes
og CallableFuture
hvilket er en god pointe
for yderligere undersøgelse af emnet.