9 feil du bør unngå når du programmerer i Java
Hvilke feil bør man unngå når man programmerer i Java? I det følgende besvarer vi dette spørsmålet.
Les første del av bloggserien vår om samtidighet i Java. I den følgende artikkelen skal vi se nærmere på forskjellene mellom tråd og prosess, trådbassenger, eksekutorer og mye mer!
Generelt er den konvensjonelle programmeringstilnærmingen sekvensiell. Alt i et program skjer ett steg om gangen.
Men faktisk er det parallellkjøringen som styrer hele verden - det er evnen til å utføre mer enn én oppgave samtidig.
For å diskutere avanserte emner som samtidighet i Java eller flertråding, må vi bli enige om noen felles definisjoner for å være sikre på at vi er på samme side.
La oss begynne med det grunnleggende. I den ikke-sekvensielle verdenen har vi to typer samtidighetsrepresentanter: prosesser og
tråder. En prosess er en instans av programmet som kjører. Normalt er den isolert fra andre prosesser.
Operativsystemet er ansvarlig for å tildele ressurser til hver prosess. I tillegg fungerer det som en dirigent som
planlegger og kontrollerer dem.
En tråd er en slags prosess, men på et lavere nivå, derfor kalles den også lett tråd. Flere tråder kan kjøre i én
prosess. Her fungerer programmet som en planlegger og en kontrollør for tråder. På denne måten ser det ut til at de enkelte programmene gjør
flere oppgaver samtidig.
Den grunnleggende forskjellen mellom tråder og prosesser er isolasjonsnivået. Prosessen har sitt eget sett med
ressurser, mens tråden deler data med andre tråder. Det kan virke som en feilutsatt tilnærming, og det er det også. For
Nå skal vi ikke fokusere på det, for det ligger utenfor denne artikkelen.
Prosesser, tråder - ok ... Men hva er egentlig samtidighet? Samtidighet betyr at du kan utføre flere oppgaver samtidig.
tid. Det betyr ikke at disse oppgavene må kjøres samtidig - det er det som er parallellisme. Concurrenc i Javay gjør heller ikke
krever at du har flere CPU-er eller til og med flere kjerner. Det kan oppnås i et enkeltkjernemiljø ved å utnytte
bytte av kontekst.
Et begrep som er relatert til samtidighet, er multithreading. Dette er en egenskap ved programmer som gjør at de kan utføre flere oppgaver samtidig. Ikke alle programmer bruker denne tilnærmingen, men de som gjør det, kan kalles flertrådede.
Vi er nesten klare, bare én definisjon til. Asynkronitet betyr at et program utfører ikke-blokkerende operasjoner.
Den setter i gang en oppgave og fortsetter med andre ting mens den venter på responsen. Når den får responsen, kan den reagere på den.
Som standard vil hver Java-applikasjon kjører i én prosess. I denne prosessen er det én tråd knyttet til main()
metode for
en applikasjon. Som nevnt er det imidlertid mulig å utnytte mekanismene for flere tråder i en og samme
program.
Tråd
er en Java klassen der magien skjer. Dette er objektrepresentasjonen av den tidligere nevnte tråden. Til
lage din egen tråd, kan du utvide Tråd
klasse. Det er imidlertid ikke en anbefalt tilnærming. Tråder
skal brukes som en mekanisme som kjører oppgaven. Oppgaver er deler av kode som vi ønsker å kjøre i samtidig modus. Vi kan definere dem ved hjelp av Kjørbar
grensesnitt.
Men nok teori, la oss sette koden der vi snakker.
Anta at vi har et par matriser med tall. For hver matrise ønsker vi å vite summen av tallene i en matrise. La oss
Vi later som om det finnes mange slike matriser, og hver av dem er relativt store. Under slike forhold ønsker vi å utnytte samtidighet og summere hver matrise som en separat oppgave.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 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 av koden ovenfor Kjørbar
er et funksjonelt grensesnitt. Det inneholder en enkelt abstrakt metode run()
uten argumenter. Den Kjørbar
grensesnittet bør implementeres av alle klasser hvis instanser er ment å være
utført av en tråd.
Når du har definert en oppgave, kan du opprette en tråd for å kjøre den. Dette kan gjøres via ny tråd()
konstruktør som
tar Kjørbar
som sitt argument.
Det siste trinnet er å start()
en nyopprettet tråd. I API-et finnes det også run()
metoder i Kjørbar
og iTråd
. Det er imidlertid ikke en måte å utnytte samtidighet på i Java. Et direkte anrop til hver av disse metodene resulterer i
utføre oppgaven i samme tråd som main()
metoden kjører.
Når det er mange oppgaver, er det ikke lurt å opprette en egen tråd for hver av dem. Å opprette en Tråd
er en
tungvint operasjon, og det er langt bedre å gjenbruke eksisterende tråder enn å opprette nye.
Når et program oppretter mange kortvarige tråder, er det bedre å bruke et trådbasseng. Trådbassenget inneholder et antall
som er klare til å kjøre, men som for øyeblikket ikke er aktive. Å gi en Kjørbar
til poolen fører til at en av trådene kallerrun()
metode for gitt Kjørbar
. Etter at en oppgave er fullført, eksisterer tråden fortsatt og er i en inaktiv tilstand.
Ok, du skjønner det - du foretrekker trådpooler i stedet for manuell oppretting. Men hvordan kan du bruke trådbassenger? Det Eksekutorer
klassen har en rekke statiske fabrikkmetoder for konstruksjon av trådpooler. For eksempel newCachedThredPool()
skaper
en pool der nye tråder opprettes etter behov, og inaktive tråder beholdes i 60 sekunder. I motsetning til dette,newFixedThreadPool()
inneholder et fast sett med tråder, der inaktive tråder beholdes på ubestemt tid.
La oss se hvordan det kan fungere i vårt eksempel. Nå trenger vi ikke å opprette tråder manuelt. I stedet må vi oppretteExecutorService
som gir en pool av tråder. Deretter kan vi tildele oppgaver til den. Det siste trinnet er å lukke tråden
for å unngå minnelekkasjer. Resten av den forrige koden forblir den samme.
ExecutorService executor = Executors.newCachedThreadPool();
executor.submit(oppgave1);
executor.submit(oppgave2);
executor.submit(oppgave3);
executor.shutdown();
Kjørbar
virker som en smart måte å opprette samtidige oppgaver på, men den har en stor svakhet. Den kan ikke returnere noen
verdi. Dessuten kan vi ikke avgjøre om en oppgave er ferdig eller ikke. Vi vet heller ikke om den er fullført
normalt eller unntaksvis. Løsningen på disse problemene er Kan kalles
.
Kan kalles
ligner på Kjørbar
på en måte også asynkrone oppgaver. Hovedforskjellen er at den er i stand til å
returnerer en verdi. Returverdien kan være av en hvilken som helst (ikke-primitiv) type, ettersom Kan kalles
grensesnittet er en parametrisert type.Kan kalles
er et funksjonelt grensesnitt som har call()
metode som kan kaste en Unntak
.
La oss nå se hvordan vi kan utnytte Kan kalles
i matriseproblemet vårt.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 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();
Ok, vi kan se hvordan Kan kalles
opprettes og sendes deretter til ExecutorService
. Men hva pokker er Fremtiden
?Fremtiden
fungerer som en bro mellom trådene. Summen av hver matrise produseres i en separat tråd, og vi trenger en måte å
få disse resultatene tilbake til main()
.
For å hente resultatet fra Fremtiden
må vi ringe get()
metode. Her kan en av to ting skje. For det første kan
resultatet av beregningen utført av Kan kalles
er tilgjengelig. Da får vi det umiddelbart. For det andre er resultatet ikke
klar ennå. I så fall get()
metoden vil blokkere til resultatet er tilgjengelig.
Problemet med Fremtiden
er at den fungerer i "push-paradigmet". Når du bruker Fremtiden
må du være som en sjef som
spør hele tiden: "Er oppgaven din ferdig? Er den klar?" helt til den gir et resultat. Å handle under konstant press er
dyrt. En mye bedre tilnærming ville være å bestille Fremtiden
hva den skal gjøre når den er ferdig med oppgaven sin. Dessverre,Fremtiden
kan ikke gjøre det, men ComputableFuture
kan.
ComputableFuture
fungerer i "pull-paradigmet". Vi kan fortelle den hva den skal gjøre med resultatet når den er ferdig med oppgavene sine. Det
er et eksempel på en asynkron tilnærming.
ComputableFuture
fungerer perfekt med Kjørbar
men ikke med Kan kalles
. I stedet er det mulig å levere en oppgave tilComputableFuture
i form av Leverandør
.
La oss se hvordan dette henger sammen med problemet vårt.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 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 som slår deg er hvor mye kortere denne løsningen er. I tillegg ser det også pent og ryddig ut.
Oppgave til CompletableFuture
kan leveres av supplyAsync()
metode som tar Leverandør
eller av runAsync()
at
tar Kjørbar
. En tilbakekalling - et stykke kode som skal kjøres når oppgaven er fullført - defineres av thenAccept()
metode.
Java gir mange forskjellige tilnærminger til samtidighet. I denne artikkelen har vi så vidt berørt temaet.
Likevel har vi dekket det grunnleggende om Tråd
, Kjørbar
, Kan kalles
, og CallableFuture
noe som er et godt poeng
for videre utforskning av temaet.