Java-samtidighedsspørgsmål (+ svar)

Denne artikel er en del af en serie: • Interviewsspørgsmål om Java Collections

• Spørgsmål om Java Type System Interview

• Java-spørgsmål om samtidighedssamtaler (+ svar) (aktuelle artikel) • Interviewspørgsmål om Java-klassestruktur og initialisering

• Java 8 interviewspørgsmål (+ svar)

• Hukommelsesstyring i Java Interview-spørgsmål (+ svar)

• Interviews med Java Generics (+ svar)

• Interviewspørgsmål til Java Flow Control (+ svar)

• Spørgsmål om Java-undtagelser (+ svar)

• Spørgsmål om Java-annotationer (+ svar)

• Top forårssamarbejdsspørgsmål

1. Introduktion

Samtidighed i Java er et af de mest komplekse og avancerede emner, der er rejst under tekniske interviews. Denne artikel giver svar på nogle af interviewspørgsmålene om det emne, du kan støde på.

Q1. Hvad er forskellen mellem en proces og en tråd?

Både processer og tråde er enheder af samtidighed, men de har en grundlæggende forskel: processer deler ikke en fælles hukommelse, mens tråde gør.

Fra operativsystemets synspunkt er en proces et uafhængigt stykke software, der kører i sit eget virtuelle hukommelsesrum. Ethvert multitasking-operativsystem (hvilket betyder næsten ethvert moderne operativsystem) skal adskille processer i hukommelsen, så en fejlsproces ikke trækker alle andre processer ned ved at kryptere fælles hukommelse.

Processerne er således normalt isoleret, og de samarbejder ved hjælp af kommunikation mellem processer, der er defineret af operativsystemet som en slags mellemliggende API.

Tværtimod er en tråd en del af et program, der deler en fælles hukommelse med andre tråde i samme applikation. Brug af fælles hukommelse gør det muligt at barbere masser af overhead, designe trådene til at samarbejde og udveksle data mellem dem meget hurtigere.

Q2. Hvordan kan du oprette en trådforekomst og køre den?

For at oprette en forekomst af en tråd har du to muligheder. Først skal du passere en Kan køres instans til sin konstruktør og kalde Start(). Kan køres er en funktionel grænseflade, så den kan overføres som et lambda-udtryk:

Tråd tråd1 = ny tråd (() -> System.out.println ("Hello World from Runnable!")); thread1.start ();

Tråden implementerer også Kan køres, så en anden måde at starte en tråd på er at oprette en anonym underklasse, tilsidesætte dens løb() metode, og ring derefter op Start():

Tråd tråd2 = ny tråd () {@Override public void run () {System.out.println ("Hej verden fra underklasse!"); }}; thread2.start ();

Q3. Beskriv de forskellige staters tråd, og hvornår opstår statsovergangene.

Tilstanden for en Tråd kan kontrolleres ved hjælp af Thread.getState () metode. Forskellige tilstande af en Tråd er beskrevet i Tråd. Stat enum. De er:

  • NY - en ny Tråd forekomst, der endnu ikke var startet via Thread.start ()
  • KØRSEL - en løbende tråd. Det kaldes køreligt, fordi det til enhver tid kan køre eller vente på det næste tidsrum fra trådplanlæggeren. EN NY tråd kommer ind i KØRSEL angive, når du ringer Thread.start () på det
  • BLOKERET - en kørende tråd bliver blokeret, hvis den skal indtaste et synkroniseret afsnit, men kan ikke gøre det på grund af en anden tråd, der holder skærmen i dette afsnit
  • VENTER - en tråd går ind i denne tilstand, hvis den venter på, at en anden tråd udfører en bestemt handling. For eksempel går en tråd ind i denne tilstand, når den kalder Object.wait () metode på en skærm, den indeholder, eller Thread.join () metode på en anden tråd
  • TIMED_WAITING - samme som ovenstående, men en tråd går ind i denne tilstand efter at have kaldt tidsbestemte versioner af Thread.sleep (), Object.wait (), Thread.join () og nogle andre metoder
  • AFSLUTTET - en tråd har afsluttet udførelsen af ​​dens Runnable.run () metode og afsluttet

Q4. Hvad er forskellen mellem de kørbare og kaldbare grænseflader? Hvordan bruges de?

Det Kan køres interface har en enkelt løb metode. Det repræsenterer en beregningsenhed, der skal køres i en separat tråd. Det Kan køres interface tillader ikke denne metode at returnere værdi eller kaste ukontrollerede undtagelser.

Det Kan kaldes interface har en enkelt opkald metode og repræsenterer en opgave, der har en værdi. Det er derfor opkald metode returnerer en værdi. Det kan også kaste undtagelser. Kan kaldes bruges generelt i ExecutorService tilfælde for at starte en asynkron opgave og derefter kalde den returnerede Fremtid eksempel for at få sin værdi.

Q5. Hvad er en dæmontråd, hvad er dens brugstilfælde? Hvordan kan du oprette en Daemon-tråd?

En dæmontråd er en tråd, der ikke forhindrer JVM i at komme ud. Når alle ikke-dæmontråde afsluttes, forlader JVM simpelthen alle resterende dæmontråde. Daemon-tråde bruges normalt til at udføre nogle understøttende eller serviceopgaver for andre tråde, men du skal tage i betragtning, at de til enhver tid kan blive opgivet.

For at starte en tråd som en dæmon skal du bruge setDaemon () metode før du ringer Start():

Tråd-dæmon = ny tråd (() -> System.out.println ("Hej fra dæmon!")); daemon.setDaemon (sand); daemon.start ();

Mærkeligt, hvis du kører dette som en del af hoved () metode, bliver beskeden muligvis ikke udskrevet. Dette kunne ske, hvis hoved () tråden afsluttes, før dæmonen når det punkt, hvor meddelelsen udskrives. Du skal generelt ikke udføre nogen I / O i dæmontråde, da de ikke engang kan udføre deres langt om længe blokerer og lukker ressourcerne, hvis de opgives.

Q6. Hvad er trådens afbrydelsesflag? Hvordan kan du indstille og kontrollere det? Hvordan har det at gøre med afbrydelse af undtagelse?

Interrupt-flag eller interrupt-status er et internt Tråd flag, der indstilles, når tråden afbrydes. For at indstille det skal du blot ringe thread.interrupt () på trådobjektet.

Hvis en tråd i øjeblikket er inde i en af ​​de metoder, der kaster Afbrudt undtagelse (vente, tilslutte, søvn osv.), så kaster denne metode straks InterruptedException. Tråden er fri til at behandle denne undtagelse i henhold til sin egen logik.

Hvis en tråd ikke er inde i en sådan metode, og thread.interrupt () kaldes, sker der ikke noget særligt. Det er trådens ansvar at regelmæssigt kontrollere afbrydelsesstatus ved hjælp af statisk tråd. afbrudt () eller instans isInterrupted () metode. Forskellen mellem disse metoder er, at statisk tråd. afbrudt () rydder afbrydelsesflagget, mens isInterrupted () gør ikke.

Q7. Hvad er eksekutor og eksekutortjeneste? Hvad er forskellen mellem disse grænseflader?

Eksekutor og ExecutorService er to relaterede grænseflader til java.util.concurrent ramme. Eksekutor er en meget enkel grænseflade med en enkelt udføre metode accepterer Kan køres instanser til udførelse. I de fleste tilfælde er dette grænsefladen, som din opgaveudførelseskode skal afhænge af.

ExecutorService udvider Eksekutor grænseflade med flere metoder til håndtering og kontrol af livscyklussen for en samtidig opgaveudførelsestjeneste (afslutning af opgaver i tilfælde af nedlukning) og metoder til mere kompleks asynkron opgavehåndtering inklusive Fremtid.

For mere information om brug Eksekutor og ExecutorService, se artiklen En guide til Java ExecutorService.

Q8. Hvad er de tilgængelige implementeringer af Executorservice i standardbiblioteket?

Det ExecutorService interface har tre standardimplementeringer:

  • ThreadPoolExecutor - til udførelse af opgaver ved hjælp af en gruppe af tråde. Når en tråd er færdig med at udføre opgaven, går den tilbage i puljen. Hvis alle tråde i puljen er optaget, skal opgaven vente på sin tur.
  • ScheduledThreadPoolExecutor giver mulighed for at planlægge udførelse af opgave i stedet for at køre den straks, når en tråd er tilgængelig. Det kan også planlægge opgaver med fast sats eller fast forsinkelse.
  • ForkJoinPool er en speciel ExecutorService til at håndtere rekursive algoritmeropgaver. Hvis du bruger en almindelig ThreadPoolExecutor for en rekursiv algoritme, vil du hurtigt finde ud af, at alle dine tråde har travlt med at vente på, at de lavere niveauer af rekursion er færdige. Det ForkJoinPool implementerer den såkaldte work-stealing algoritme, der gør det muligt at bruge tilgængelige tråde mere effektivt.

Q9. Hvad er Java Memory Model (Jmm)? Beskriv dens formål og grundlæggende ideer.

Java Memory Model er en del af Java-sprogspecifikationen beskrevet i kapitel 17.4. Det specificerer, hvordan flere tråde får adgang til fælles hukommelse i et samtidigt Java-program, og hvordan dataændringer af en tråd synliggøres for andre tråde. Selvom JMM er ret kort og kortfattet, kan det være svært at forstå uden stærk matematisk baggrund.

Behovet for hukommelsesmodel stammer fra det faktum, at den måde, din Java-kode har adgang til data på, ikke er, hvordan det rent faktisk sker på de lavere niveauer. Hukommelse skriver og læser kan omarrangeres eller optimeres af Java-compileren, JIT-compileren og endda CPU'en, så længe det observerbare resultat af disse læser og skriver er det samme.

Dette kan føre til kontraintuitive resultater, når din applikation skaleres til flere tråde, fordi de fleste af disse optimeringer tager højde for en enkelt udførelsestråd (krydstrådoptimeringerne er stadig ekstremt svære at implementere). Et andet stort problem er, at hukommelsen i moderne systemer er flerlags: flere kerner i en processor kan beholde nogle ikke-skyllede data i deres cache eller læse / skrive buffere, hvilket også påvirker tilstanden for hukommelsen, der observeres fra andre kerner.

For at gøre tingene værre, ville eksistensen af ​​forskellige hukommelsesadgangsarkitekturer bryde Java's løfte om "skriv en gang, kør overalt". Heldigvis for programmørerne specificerer JMM nogle garantier, som du kan stole på, når du designer multitrådede applikationer. At holde sig til disse garantier hjælper en programmør med at skrive multitrådet kode, der er stabil og bærbar mellem forskellige arkitekturer.

De vigtigste forestillinger om JMM er:

  • Handlinger, dette er inter-thread-handlinger, der kan udføres af en tråd og detekteres af en anden tråd, som læsning eller skrivning af variabler, låsning / oplåsning af skærme og så videre
  • Synkroniseringshandlinger, en bestemt delmængde af handlinger, som læsning / skrivning af a flygtige variabel eller låsning / oplåsning af en skærm
  • Programordre (PO), den observerbare samlede rækkefølge af handlinger inden for en enkelt tråd
  • Synkroniseringsrækkefølge (SO), den samlede rækkefølge mellem alle synkroniseringshandlinger - den skal være i overensstemmelse med programrækkefølgen, det vil sige, hvis to synkroniseringshandlinger kommer hinanden i PO, forekommer de i samme rækkefølge i SO
  • synkroniserer-med (SW) forhold mellem visse synkroniseringshandlinger, som oplåsning af skærm og låsning af den samme skærm (i en anden eller samme tråd)
  • Der sker før ordre - kombinerer PO med SW (dette kaldes transitiv lukning i sætteori) for at oprette en delvis rækkefølge af alle handlinger mellem tråde. Hvis en handling sker før en anden, så kan resultaterne af den første handling observeres af den anden handling (skriv f.eks. en variabel i en tråd og læs den i en anden)
  • Sker før konsistens - et sæt handlinger er HB-konsistent, hvis hver læsning observerer enten den sidste skrivning til den placering i den foregående rækkefølge eller anden skrivning via dataløb
  • Udførelse - et bestemt sæt ordnede handlinger og konsistensregler mellem dem

For et givet program kan vi observere flere forskellige henrettelser med forskellige resultater. Men hvis et program er korrekt synkroniseret, så ser alle dets henrettelser ud til at være sekventielt konsistent, hvilket betyder at du kan begrunde det multitrådede program som et sæt handlinger, der forekommer i en rækkefølge. Dette sparer dig besværet med at tænke på re-ordninger, optimeringer eller datacaching under hætte.

Q10. Hvad er et flygtigt felt, og hvilke garantier holder Jmm for et sådant felt?

EN flygtige felt har specielle egenskaber i henhold til Java Memory Model (se Q9). Læser og skriver om en flygtige variabel er synkroniseringshandlinger, hvilket betyder at de har en samlet rækkefølge (alle tråde vil overholde en ensartet rækkefølge af disse handlinger). En læsning af en flygtig variabel garanteres at observere den sidste skrivning til denne variabel i henhold til denne rækkefølge.

Hvis du har et felt, der er adgang til fra flere tråde, hvor mindst en tråd skriver til det, bør du overveje at gøre det flygtige, ellers er der en lille garanti for, hvad en bestemt tråd ville læse fra dette felt.

En anden garanti for flygtige er atomicitet ved at skrive og læse 64-bit værdier (lang og dobbelt). Uden en flygtig modifikator kunne en læsning af et sådant felt observere en værdi, der delvist er skrevet af en anden tråd.

Q11. Hvilke af følgende operationer er atomare?

  • skriver til en ikke-flygtigeint;
  • skriver til en flygtig int;
  • skriver til en ikke-flygtig lang;
  • skriver til en flygtig lang;
  • stigning a flygtig lang?

En skrivning til en int (32-bit) variabel er garanteret atom, uanset om den er flygtige eller ikke. EN lang (64-bit) variabel kunne skrives i to separate trin, for eksempel på 32-bit arkitekturer, så der er som standard ingen atomicitetsgaranti. Men hvis du angiver flygtige modifikator, en lang variabel garanteres at få adgang til atomisk.

Forøgelsesoperationen udføres normalt i flere trin (hentning af en værdi, ændring og tilbagskrivning), så det garanteres aldrig at være atomisk, uanset om variablen er flygtige eller ikke. Hvis du har brug for at implementere atomforøgelse af en værdi, skal du bruge klasser AtomicInteger, AtomicLong etc.

Q12. Hvilke særlige garantier har Jmm for de sidste felter i en klasse?

JVM garanterer dybest set det endelig felter i en klasse initialiseres, før en tråd får fat i objektet. Uden denne garanti kan en henvisning til et objekt blive offentliggjort, dvs. blive synlig, til en anden tråd, før alle felterne i dette objekt initialiseres på grund af omordninger eller andre optimeringer. Dette kan medføre hurtig adgang til disse felter.

Dette er grunden til, at når du opretter et uforanderligt objekt, skal du altid oprette alle dets felter endelig, selvom de ikke er tilgængelige via getter-metoder.

Q13. Hvad er betydningen af ​​et synkroniseret søgeord i definitionen af ​​en metode? af en statisk metode? Før en blokering?

Det synkroniseret nøgleord før en blok betyder, at enhver tråd, der kommer ind i denne blok, skal erhverve skærmen (objektet i parentes). Hvis skærmen allerede er erhvervet af en anden tråd, kommer den tidligere tråd ind i BLOKERET tilstand, og vent, indtil skærmen frigives.

synkroniseret (objekt) {// ...}

EN synkroniseret instansmetoden har den samme semantik, men selve forekomsten fungerer som en skærm.

synkroniseret ugyldig instansMetode () {// ...}

For en statisk synkroniseret metode, er skærmen Klasse objekt, der repræsenterer den erklærende klasse.

statisk synkroniseret ugyldigt staticMethod () {// ...}

Q14. Hvis to tråde kalder en synkroniseret metode på forskellige objektforekomster samtidigt, kan en af ​​disse tråde blokere? Hvad hvis metoden er statisk?

Hvis metoden er en instansmetode, fungerer forekomsten som en monitor for metoden. To tråde, der kalder metoden i forskellige tilfælde, erhverver forskellige skærme, så ingen af ​​dem bliver blokeret.

Hvis metoden er statisk, så er skærmen den Klasse objekt. For begge tråde er skærmen den samme, så en af ​​dem blokerer sandsynligvis og venter på, at en anden forlader synkroniseret metode.

Q15. Hvad er formålet med ventemetode, underretning og meddelelse om alle metoderne i objektklassen?

En tråd, der ejer objektets skærm (for eksempel en tråd, der er indtastet en synkroniseret sektion beskyttet af objektet) kan ringe object.wait () for midlertidigt at frigive skærmen og give andre tråde en chance for at erhverve skærmen. Dette kan f.eks. Gøres for at vente på en bestemt tilstand.

Når en anden tråd, der har erhvervet skærmen, opfylder betingelsen, kan den muligvis ringe objekt. underret () eller object.notifyAll () og slip skærmen. Det underrette metoden vækker en enkelt tråd i ventetilstand, og underret alle metode vækker alle tråde, der venter på denne skærm, og de konkurrerer alle om at få låsen igen.

Det følgende BlockingQueue implementering viser, hvordan flere tråde fungerer sammen via vent-underret mønster. Hvis vi sætte et element i en tom kø, alle tråde, der ventede i tage metode vågner op og prøver at modtage værdien. Hvis vi sætte et element i en fuld kø, sætte metode ventes til opkaldet til metode. Det metode fjerner et element og giver besked om de tråde, der venter i sætte metode, at køen har et tomt sted til et nyt element.

offentlig klasse BlockingQueue {privat liste kø = ny LinkedList (); privat int grænse = 10; offentligt synkroniseret ugyldigt sæt (T-element) {mens (kø.størrelse () == grænse) {prøv {vent (); } fange (InterruptedException e) {}} if (queue.isEmpty ()) {notifyAll (); } kø.tillæg (vare); } offentlig synkroniseret T tage () kaster InterruptedException {mens (queue.isEmpty ()) {prøv {vent (); } fange (InterruptedException e) {}} if (queue.size () == limit) {notifyAll (); } retur kø. fjern (0); }}

Q16. Beskriv betingelserne for blokering, livelock og sult. Beskriv de mulige årsager til disse forhold.

Dødlås er en betingelse inden for en gruppe tråde, der ikke kan gøre fremskridt, fordi hver tråd i gruppen er nødt til at tilegne sig en ressource, der allerede er erhvervet af en anden tråd i gruppen. Det mest enkle tilfælde er, når to tråde skal låse begge to ressourcer for at komme videre, den første ressource er allerede låst af den ene tråd og den anden af ​​den anden. Disse tråde får aldrig en lås til begge ressourcer og vil således aldrig udvikle sig.

Livelock er et tilfælde af flere tråde, der reagerer på betingelser eller begivenheder, genereret af sig selv. En begivenhed forekommer i en tråd og skal behandles af en anden tråd.Under denne behandling opstår der en ny begivenhed, som skal behandles i den første tråd osv. Sådanne tråde er levende og ikke blokeret, men gør stadig ikke fremskridt, fordi de overvælder hinanden med ubrugeligt arbejde.

Sult er et tilfælde af en tråd, der ikke kan erhverve ressource, fordi andre tråde (eller tråde) optager den for længe eller har højere prioritet. En tråd kan ikke gøre fremskridt og kan således ikke udføre nyttigt arbejde.

Q17. Beskriv formålet med og anvendelsestilfælde for gaffel / tilslutningsrammer.

Gaffel / sammenføjningsrammen tillader parallelisering af rekursive algoritmer. Hovedproblemet med parallelisering af rekursion ved hjælp af noget lignende ThreadPoolExecutor er, at du hurtigt kan løbe tør for tråde, fordi hvert rekursive trin kræver sin egen tråd, mens trådene op i stakken vil være inaktive og venter.

Indgangspunktet for fork / join-rammen er ForkJoinPool klasse, som er en implementering af ExecutorService. Det implementerer den algoritme, der stjæler arbejde, hvor ledige tråde forsøger at "stjæle" arbejde fra travle tråde. Dette gør det muligt at sprede beregningerne mellem forskellige tråde og gøre fremskridt, mens du bruger færre tråde, end det ville kræve med en sædvanlig trådpulje.

Flere oplysninger og kodeeksempler til fork / join-rammen kan findes i artiklen “Guide to the Fork / Join Framework in Java”.

Næste » Java-klassestruktur og initialiseringsspørgsmål « Tidligere spørgsmål om Java Type System Interview