Vragen over Java Concurrency-sollicitatiegesprekken (+ Antwoorden)

Dit artikel maakt deel uit van een reeks: • Interviewvragen over Java Collections

• Vragen over het Java Type System-interview

• Vragen over Java-concurrency-sollicitatiegesprekken (+ antwoorden) (huidig ​​artikel) • Vragen over Java-klassestructuur en initialisatie

• Java 8 sollicitatievragen (+ antwoorden)

• Geheugenbeheer in Java-interviewvragen (+ antwoorden)

• Java Generics sollicitatievragen (+ antwoorden)

• Vragen over Java Flow Control-interview (+ antwoorden)

• Interviewvragen over Java-uitzonderingen (+ antwoorden)

• Vragen over Java-annotaties tijdens sollicitatiegesprekken (+ antwoorden)

• Topvragen tijdens het Spring Framework-interview

1. Inleiding

Gelijktijdigheid in Java is een van de meest complexe en geavanceerde onderwerpen die tijdens technische interviews aan de orde komen. Dit artikel geeft antwoorden op enkele van de interviewvragen over het onderwerp die u kunt tegenkomen.

V1. Wat is het verschil tussen een proces en een thread?

Zowel processen als threads zijn eenheden van gelijktijdigheid, maar ze hebben een fundamenteel verschil: processen delen geen gemeenschappelijk geheugen, terwijl threads dat wel doen.

Vanuit het oogpunt van het besturingssysteem is een proces een onafhankelijk stuk software dat in zijn eigen virtuele geheugenruimte wordt uitgevoerd. Elk multitasking-besturingssysteem (wat bijna elk modern besturingssysteem betekent) moet processen in het geheugen scheiden, zodat een mislukt proces niet alle andere processen naar beneden sleept door het gemeenschappelijke geheugen te versleutelen.

De processen zijn dus meestal geïsoleerd en werken samen door middel van communicatie tussen processen die door het besturingssysteem wordt gedefinieerd als een soort tussenliggende API.

Integendeel, een thread is een onderdeel van een applicatie die een gemeenschappelijk geheugen deelt met andere threads van dezelfde applicatie. Door een gemeenschappelijk geheugen te gebruiken, kunt u veel overhead besparen, de threads zo ontwerpen dat ze samenwerken en veel sneller gegevens uitwisselen.

Q2. Hoe kunt u een threadinstantie maken en deze uitvoeren?

Om een ​​instantie van een discussielijn te maken, heb je twee opties. Geef eerst een Runnable instantie naar zijn constructor en call begin(). Runnable is een functionele interface, dus het kan worden doorgegeven als een lambda-uitdrukking:

Thread thread1 = nieuwe Thread (() -> System.out.println ("Hallo wereld van Runnable!")); thread1.start ();

Thread implementeert ook Runnable, dus een andere manier om een ​​thread te starten, is door een anonieme subklasse te maken en de bijbehorende rennen() methode en bel begin():

Thread thread2 = nieuwe Thread () {@Override public void run () {System.out.println ("Hallo wereld uit subklasse!"); }}; thread2.start ();

Q3. Beschrijf de verschillende toestanden van een draad en wanneer vinden de staatsovergangen plaats.

De staat van een Draad kan worden gecontroleerd met de Thread.getState () methode. Verschillende staten van een Draad worden beschreven in de Thread.State opsomming. Zij zijn:

  • NIEUW - een nieuwe Draad instantie die nog niet was gestart via Thread.start ()
  • UITVOERBAAR - een lopende draad. Het wordt uitvoerbaar genoemd omdat het op elk willekeurig moment kan draaien of wachten op de volgende hoeveelheid tijd van de threadplanner. EEN NIEUW draad voert de UITVOERBAAR staat wanneer u belt Thread.start () ben ermee bezig
  • GEBLOKKEERD - een lopende thread wordt geblokkeerd als deze een gesynchroniseerde sectie moet binnengaan, maar kan dat niet doen vanwege een andere thread die de monitor van deze sectie vasthoudt
  • AAN HET WACHTEN - een thread komt in deze toestand als het wacht op een andere thread om een ​​bepaalde actie uit te voeren. Een thread komt bijvoorbeeld deze status binnen bij het aanroepen van de Object.wait () methode op een monitor die het bevat, of de Thread.join () methode op een andere thread
  • TIMED_WAITING - hetzelfde als hierboven, maar een thread komt in deze toestand na het aanroepen van getimede versies van Thread.sleep (), Object.wait (), Thread.join () en enkele andere methoden
  • BEËINDIGD - een thread heeft de uitvoering van zijn Runnable.run () methode en beëindigd

V4. Wat is het verschil tussen de uitvoerbare en oproepbare interfaces? Hoe worden ze gebruikt?

De Runnable interface heeft een enkele rennen methode. Het vertegenwoordigt een rekeneenheid die in een aparte thread moet worden uitgevoerd. De Runnable interface staat deze methode niet toe om waarde te retourneren of om ongecontroleerde uitzonderingen te genereren.

De Oproepbaar interface heeft een enkele bellen methode en vertegenwoordigt een taak die een waarde heeft. Dat is waarom de bellen methode geeft een waarde terug. Het kan ook uitzonderingen opleveren. Oproepbaar wordt over het algemeen gebruikt in ExecutorService instanties om een ​​asynchrone taak te starten en vervolgens het geretourneerde Toekomst instantie om de waarde ervan te krijgen.

V5. Wat is een daemon-thread, wat zijn de toepassingen? Hoe kun je een daemon-thread maken?

Een daemon-thread is een thread die niet verhindert dat JVM wordt afgesloten. Wanneer alle niet-daemon-threads zijn beëindigd, verlaat de JVM eenvoudig alle resterende daemon-threads. Daemon-threads worden meestal gebruikt om ondersteunende of servicetaken voor andere threads uit te voeren, maar u moet er rekening mee houden dat ze op elk moment kunnen worden verlaten.

Om een ​​thread als een daemon te starten, moet u de setDaemon () methode voordat u belt begin():

Thread daemon = nieuwe Thread (() -> System.out.println ("Hallo van daemon!")); daemon.setDaemon (true); daemon.start ();

Vreemd genoeg, als je dit uitvoert als onderdeel van het hoofd() methode, wordt het bericht mogelijk niet afgedrukt. Dit kan gebeuren als het hoofd() thread zou eindigen voordat de daemon op het punt zou komen om het bericht af te drukken. Over het algemeen zou je geen I / O in daemon-threads moeten doen, omdat ze hun Tenslotte blokkeert en sluit de bronnen als ze worden verlaten.

V6. Wat is de onderbrekingsvlag van de discussielijn? Hoe kunt u dit instellen en controleren? Hoe verhoudt het zich tot de onderbreking van de uitzondering?

De interruptvlag, of interruptstatus, is een internal Draad vlag die wordt ingesteld wanneer de thread wordt onderbroken. Om het in te stellen, hoeft u alleen maar te bellen thread.interrupt () op het thread-object.

Als een thread zich momenteel binnen een van de methoden bevindt die InterruptedException (wacht, toetreden, slaap etc.), dan gooit deze methode onmiddellijk InterruptedException. De thread is vrij om deze uitzondering volgens zijn eigen logica te verwerken.

Als een draad niet binnen een dergelijke methode en thread.interrupt () wordt genoemd, gebeurt er niets bijzonders. Het is de verantwoordelijkheid van de thread om periodiek de interruptstatus te controleren met statische Thread.interrupted () of instantie isInterrupted () methode. Het verschil tussen deze methoden is dat de statische Thread.interrupted () wist de interruptvlag, while isInterrupted () doet niet.

V7. Wat zijn uitvoerder en uitvoerder? Wat zijn de verschillen tussen deze interfaces?

Uitvoerder en ExecutorService zijn twee gerelateerde interfaces van java.util.concurrent kader. Uitvoerder is een heel eenvoudige interface met een enkele uitvoeren methode accepteren Runnable instanties voor uitvoering. In de meeste gevallen is dit de interface waarvan uw taakuitvoerende code afhankelijk moet zijn.

ExecutorService breidt de Uitvoerder interface met meerdere methoden voor het afhandelen en controleren van de levenscyclus van een gelijktijdige taakuitvoerdienst (beëindiging van taken in geval van uitschakeling) en methoden voor meer complexe asynchrone taakafhandeling, waaronder Futures.

Voor meer informatie over het gebruik van Uitvoerder en ExecutorService, zie het artikel A Guide to Java ExecutorService.

V8. Wat zijn de beschikbare implementaties van Executorservice in de standaardbibliotheek?

De ExecutorService interface heeft drie standaardimplementaties:

  • ThreadPoolExecutor - voor het uitvoeren van taken met een pool van threads. Zodra een thread klaar is met het uitvoeren van de taak, gaat deze terug naar de pool. Als alle threads in de pool bezet zijn, moet de taak op zijn beurt wachten.
  • ScheduledThreadPoolExecutor maakt het mogelijk om de uitvoering van taken te plannen in plaats van deze onmiddellijk uit te voeren wanneer een thread beschikbaar is. Het kan ook taken plannen met een vast tarief of een vaste vertraging.
  • ForkJoinPool is een bijzonder ExecutorService voor het omgaan met recursieve algoritmetaken. Als u een gewone ThreadPoolExecutor voor een recursief algoritme zul je snel merken dat al je threads bezig zijn te wachten tot de lagere niveaus van recursie zijn voltooid. De ForkJoinPool implementeert het zogenaamde work-stealing-algoritme waarmee het beschikbare threads efficiënter kan gebruiken.

V9. Wat is een Java-geheugenmodel (Jmm)? Beschrijf het doel en de basisideeën.

Java-geheugenmodel is een onderdeel van de Java-taalspecificatie die wordt beschreven in hoofdstuk 17.4. Het specificeert hoe meerdere threads toegang krijgen tot gemeenschappelijk geheugen in een gelijktijdige Java-toepassing, en hoe gegevenswijzigingen door één thread zichtbaar worden gemaakt voor andere threads. Hoewel het vrij kort en beknopt is, is JMM misschien moeilijk te bevatten zonder sterke wiskundige achtergrond.

De behoefte aan een geheugenmodel komt voort uit het feit dat de manier waarop uw Java-code toegang krijgt tot gegevens, niet is zoals het werkelijk gebeurt op de lagere niveaus. Schrijf- en leesbewerkingen in het geheugen kunnen opnieuw worden gerangschikt of geoptimaliseerd door de Java-compiler, JIT-compiler en zelfs de CPU, zolang het waarneembare resultaat van deze lees- en schrijfbewerkingen hetzelfde is.

Dit kan leiden tot contra-intuïtieve resultaten wanneer uw toepassing wordt geschaald naar meerdere threads, omdat de meeste van deze optimalisaties rekening houden met een enkele uitvoeringsdraad (de cross-thread optimizers zijn nog steeds buitengewoon moeilijk te implementeren). Een ander groot probleem is dat het geheugen in moderne systemen uit meerdere lagen bestaat: meerdere kernen van een processor kunnen sommige niet-doorgespoelde gegevens in hun caches of lees- / schrijfbuffers bewaren, wat ook van invloed is op de toestand van het geheugen dat wordt waargenomen vanaf andere kernen.

Om de zaken nog erger te maken, zou het bestaan ​​van verschillende geheugentoegangsarchitecturen de Java's belofte van "één keer schrijven, overal uitvoeren" breken. Gelukkig voor de programmeurs specificeert de JMM enkele garanties waarop u kunt vertrouwen bij het ontwerpen van multithreaded applicaties. Door aan deze garanties vast te houden, kan een programmeur multithreaded code schrijven die stabiel en draagbaar is tussen verschillende architecturen.

De belangrijkste begrippen van JMM zijn:

  • Acties, dit zijn acties tussen de threads die door de ene thread kunnen worden uitgevoerd en door een andere thread kunnen worden gedetecteerd, zoals het lezen of schrijven van variabelen, het vergrendelen / ontgrendelen van monitoren enzovoort
  • Synchronisatie-acties, een bepaalde subset van acties, zoals lezen / schrijven van een vluchtig variabel, of het vergrendelen / ontgrendelen van een monitor
  • Programmavolgorde (PO), de waarneembare totale volgorde van acties binnen een enkele thread
  • Synchronisatie volgorde (SO), de totale volgorde tussen alle synchronisatieacties - deze moet consistent zijn met Program Order, dat wil zeggen, als twee synchronisatieacties voor elkaar komen in PO, vinden ze in dezelfde volgorde plaats in SO
  • synchroniseert met (SW) relatie tussen bepaalde synchronisatieacties, zoals het ontgrendelen van de monitor en het vergrendelen van dezelfde monitor (in een andere of dezelfde thread)
  • Gebeurt vóór bestelling - combineert PO met SW (dit heet transitieve sluiting in de verzamelingenleer) om een ​​gedeeltelijke ordening van alle acties tussen threads te creëren. Als een actie gebeurt-voor een andere, dan zijn de resultaten van de eerste actie waarneembaar door de tweede actie (schrijf bijvoorbeeld een variabele in de ene thread en lees in een andere)
  • Gebeurt vóór consistentie - een reeks acties is HB-consistent als bij elke leesbewerking de laatste schrijfactie naar die locatie in de volgorde gebeurt, of een andere schrijfactie via gegevensrace wordt waargenomen
  • Executie - een bepaalde reeks geordende acties en consistentieregels daartussen

Voor een bepaald programma kunnen we meerdere verschillende uitvoeringen observeren met verschillende resultaten. Maar als een programma is correct gesynchroniseerd, dan lijken al zijn executies te zijn opeenvolgend consistent, wat betekent dat u over het multithreaded programma kunt redeneren als een reeks acties die in een bepaalde volgorde plaatsvinden. Dit bespaart u de moeite om na te denken over herordeningen, optimalisaties of gegevenscaching onder de motorkap.

V10. Wat is een vluchtig veld en welke garanties biedt de Jmm voor een dergelijk veld?

EEN vluchtig veld heeft speciale eigenschappen volgens het Java-geheugenmodel (zie Q9). Het leest en schrijft van een vluchtig variabele zijn synchronisatieacties, wat betekent dat ze een totale volgorde hebben (alle threads zullen een consistente volgorde van deze acties aanhouden). Bij het lezen van een vluchtige variabele wordt gegarandeerd de laatste schrijfactie naar deze variabele geobserveerd, volgens deze volgorde.

Als je een veld hebt dat toegankelijk is vanuit meerdere threads, en waar ten minste één thread naartoe schrijft, dan zou je moeten overwegen om het te maken vluchtig, of anders is er een kleine garantie voor wat een bepaalde thread uit dit veld zou lezen.

Nog een garantie voor vluchtig is atomiciteit van het schrijven en lezen van 64-bits waarden (lang en dubbele). Zonder een vluchtige modifier zou een lezing van een dergelijk veld een waarde kunnen waarnemen die gedeeltelijk door een andere thread is geschreven.

V11. Welke van de volgende bewerkingen zijn atomair?

  • schrijven naar een niet-vluchtigint;
  • schrijven naar een vluchtige int;
  • schrijven naar een niet-vluchtig lang;
  • schrijven naar een vluchtig lang;
  • het verhogen van een vluchtig lang?

Een schrijven naar een int (32-bits) variabele is gegarandeerd atomair, of dat nu het geval is vluchtig of niet. EEN lang (64-bits) variabele kan in twee afzonderlijke stappen worden geschreven, bijvoorbeeld op 32-bits architecturen, dus standaard is er geen atomiciteitsgarantie. Als u echter het vluchtig modifier, een lang variabele is gegarandeerd atomair toegankelijk.

De ophogingsbewerking wordt meestal in meerdere stappen uitgevoerd (een waarde ophalen, wijzigen en terugschrijven), dus het is nooit gegarandeerd dat deze atomisch is, of de variabele nu vluchtig of niet. Als u een atomaire verhoging van een waarde moet implementeren, moet u klassen gebruiken AtomicInteger, AtomicLong enz.

V12. Welke speciale garanties biedt de Jmm voor laatste velden van een klas?

JVM garandeert dat in principe laatste velden van een klasse worden geïnitialiseerd voordat een thread het object in handen krijgt. Zonder deze garantie kan een verwijzing naar een object worden gepubliceerd, d.w.z. zichtbaar worden, naar een andere thread voordat alle velden van dit object zijn geïnitialiseerd, vanwege herschikkingen of andere optimalisaties. Dit kan snelle toegang tot deze velden veroorzaken.

Dit is de reden waarom u bij het maken van een onveranderlijk object altijd al zijn velden moet maken laatste, zelfs als ze niet toegankelijk zijn via getter-methoden.

V13. Wat is de betekenis van een gesynchroniseerd trefwoord in de definitie van een methode? van een statische methode? Voor een blok?

De gesynchroniseerd trefwoord voor een blok betekent dat elke thread die dit blok binnenkomt, de monitor moet verwerven (het object tussen haakjes). Als de monitor al is verkregen door een andere thread, komt de vorige thread in het GEBLOKKEERD staat en wacht tot de monitor wordt losgelaten.

gesynchroniseerd (object) {// ...}

EEN gesynchroniseerd instantiemethode heeft dezelfde semantiek, maar de instantie zelf fungeert als een monitor.

gesynchroniseerde ongeldige instanceMethod () {// ...}

Voor een statisch gesynchroniseerd methode, is de monitor de Klasse object dat de declarerende klasse vertegenwoordigt.

statisch gesynchroniseerd void staticMethod () {// ...}

V14. Als twee threads tegelijkertijd een gesynchroniseerde methode op verschillende objectinstanties aanroepen, kan een van deze threads dan worden geblokkeerd? Wat moet ik doen als de methode statisch is?

Als de methode een instantiemethode is, fungeert de instantie als een monitor voor de methode. Twee threads die de methode op verschillende instanties aanroepen, verwerven verschillende monitoren, zodat geen van deze wordt geblokkeerd.

Als de methode is statisch, dan is de monitor de Klasse voorwerp. Voor beide threads is de monitor hetzelfde, dus een van hen zal waarschijnlijk blokkeren en wachten tot een andere het gesynchroniseerd methode.

V15. Wat is het doel van het wachten, informeren en informeren van alle methoden van de objectklasse?

Een thread die eigenaar is van de monitor van het object (bijvoorbeeld een thread die een gesynchroniseerd sectie bewaakt door het object) mogen bellen object.wait () om de monitor tijdelijk vrij te geven en andere threads de kans te geven de monitor te bemachtigen. Dit kan bijvoorbeeld worden gedaan om op een bepaalde aandoening te wachten.

Wanneer een andere thread die de monitor heeft verkregen, aan de voorwaarde voldoet, kan deze bellen object.notify () of object.notifyAll () en laat de monitor los. De op de hoogte brengen methode wekt een enkele thread in de wachttoestand, en de informerenAll methode wekt alle threads die op deze monitor wachten, en ze strijden allemaal om het opnieuw verkrijgen van de vergrendeling.

Het volgende BlockingQueue implementatie laat zien hoe meerdere threads samenwerken via de wacht-op de hoogte patroon. Als wij leggen een element in een lege wachtrij, alle threads die in de nemen methode wakker worden en proberen om de waarde te ontvangen. Als wij leggen een element in een volledige wachtrij, het leggen methode wachts voor de oproep naar de krijgen methode. De krijgen methode verwijdert een element en meldt de threads die wachten in het leggen methode dat de wachtrij een lege plaats heeft voor een nieuw item.

openbare klasse BlockingQueue {wachtrij privélijst = nieuwe LinkedList (); private int limiet = 10; openbare gesynchroniseerde ongeldige put (T-item) {while (queue.size () == limit) {probeer {wait (); } catch (InterruptedException e) {}} if (queue.isEmpty ()) {notificationAll (); } queue.add (item); } openbare gesynchroniseerde T take () gooit InterruptedException {while (queue.isEmpty ()) {try {wait (); } catch (InterruptedException e) {}} if (queue.size () == limit) {notificationAll (); } return queue.remove (0); }}

V16. Beschrijf de omstandigheden van Deadlock, Livelock en Starvation. Beschrijf de mogelijke oorzaken van deze aandoeningen.

Impasse is een voorwaarde binnen een groep threads die geen vooruitgang kan boeken omdat elke thread in de groep een bron moet verwerven die al is verkregen door een andere thread in de groep. Het eenvoudigste geval is wanneer twee threads beide bronnen moeten vergrendelen om verder te kunnen gaan, de eerste resource al is vergrendeld door de ene thread en de tweede door een andere. Deze threads krijgen nooit een vergrendeling voor beide bronnen en zullen dus nooit verder gaan.

Livelock is een geval van meerdere threads die reageren op door henzelf gegenereerde omstandigheden of gebeurtenissen. Een event komt voor in de ene thread en moet door een andere thread worden verwerkt.Tijdens deze verwerking vindt een nieuwe gebeurtenis plaats die in de eerste thread moet worden verwerkt, enzovoort. Zulke draden zijn levend en niet geblokkeerd, maar boeken toch geen vooruitgang omdat ze elkaar overweldigen met nutteloos werk.

Verhongering is een geval van een thread die geen bron kan verkrijgen omdat andere thread (of threads) deze te lang bezetten of een hogere prioriteit hebben. Een thread kan geen vooruitgang boeken en kan dus geen nuttig werk vervullen.

V17. Beschrijf het doel en de use-cases van het Fork / Join Framework.

Het fork / join-raamwerk maakt parallellisatie van recursieve algoritmen mogelijk. Het grootste probleem met parallellisatie van recursie met behulp van zoiets als ThreadPoolExecutor is dat u snel zonder threads kunt komen te zitten omdat elke recursieve stap zijn eigen thread vereist, terwijl de threads op de stapel inactief zijn en wachten.

Het ingangspunt van het fork / join-framework is het ForkJoinPool class die een implementatie is van ExecutorService. Het implementeert het algoritme voor het stelen van werk, waarbij inactieve threads werk proberen te "stelen" van drukke threads. Dit maakt het mogelijk om de berekeningen over verschillende threads te spreiden en vooruitgang te boeken terwijl u minder threads gebruikt dan nodig zou zijn met een gebruikelijke threadpool.

Meer informatie en codevoorbeelden voor het fork / join-framework zijn te vinden in het artikel "Gids voor het Fork / Join-framework in Java".

De volgende » Vragen over Java-klassestructuur en initialisatie « Vorige interviewvragen over Java Type System