CodeGym /Blog Java /Aleatoriu /Top 50 de întrebări și răspunsuri la interviul de angajar...
John Squirrels
Nivel
San Francisco

Top 50 de întrebări și răspunsuri la interviul de angajare pentru Java Core. Partea 2

Publicat în grup
Top 50 de întrebări și răspunsuri la interviul de angajare pentru Java Core. Partea 1Top 50 de întrebări și răspunsuri la interviul de angajare pentru Java Core.  Partea 2 - 1

Multithreading

24. Cum creez un fir nou în Java?

Într-un fel sau altul, un thread este creat folosind clasa Thread. Dar există mai multe moduri de a face asta...
  1. Moșteniți java.lang.Thread .
  2. Implementați interfața java.lang.Runnable — constructorul clasei Thread preia un obiect Runnable.
Să vorbim despre fiecare dintre ele.

Moșteniți clasa Thread

În acest caz, facem ca clasa noastră să moștenească java.lang.Thread . Are o metodă run() și tocmai de asta avem nevoie. Toată viața și logica noului fir va fi în această metodă. Este un fel ca o metodă principală pentru noul thread. După aceea, tot ce rămâne este să creăm un obiect din clasa noastră și să apelăm metoda start() . Acest lucru va crea un fir nou și va începe să-și execute logica. Hai să aruncăm o privire:

/**
* An example of how to create threads by inheriting the {@link Thread} class.
*/
class ThreadInheritance extends Thread {

   @Override
   public void run() {
       System.out.println(Thread.currentThread().getName());
   }

   public static void main(String[] args) {
       ThreadInheritance threadInheritance1 = new ThreadInheritance();
       ThreadInheritance threadInheritance2 = new ThreadInheritance();
       ThreadInheritance threadInheritance3 = new ThreadInheritance();
       threadInheritance1.start();
       threadInheritance2.start();
       threadInheritance3.start();
   }
}
Ieșirea consolei va fi cam așa:
Firul-1 Firul-0 Firul-2
Adică, chiar și aici vedem că firele de execuție nu sunt executate în ordine, ci mai degrabă așa cum JVM-ul consideră de cuviință să le ruleze :)

Implementați interfața Runnable

Dacă sunteți împotriva moștenirii și/sau moșteniți deja o altă clasă, puteți utiliza interfața java.lang.Runnable . Aici, facem ca clasa noastră să implementeze această interfață prin implementarea metodei run() , la fel ca în exemplul de mai sus. Tot ce rămâne este să creați obiecte Thread . S-ar părea că mai multe linii de cod sunt mai rele. Dar știm cât de pernicioasă este moștenirea și că este mai bine să o evităm prin toate mijloacele ;) Aruncă o privire:

/**
* An example of how to create threads from the {@link Runnable} interface.
* It's easier than easy — we implement this interface and then pass an instance of our object
* to the constructor.
*/
class ThreadInheritance implements Runnable {

   @Override
   public void run() {
       System.out.println(Thread.currentThread().getName());
   }

   public static void main(String[] args) {
       ThreadInheritance runnable1 = new ThreadInheritance();
       ThreadInheritance runnable2 = new ThreadInheritance();
       ThreadInheritance runnable3 = new ThreadInheritance();

       Thread threadRunnable1 = new Thread(runnable1);
       Thread threadRunnable2 = new Thread(runnable2);
       Thread threadRunnable3 = new Thread(runnable3);

       threadRunnable1.start();
       threadRunnable2.start();
       threadRunnable3.start();
   }
}
Și iată rezultatul:
Firul-0 Firul-1 Firul-2

25. Care este diferența dintre un proces și un fir?

Top 50 de întrebări și răspunsuri la interviul de angajare pentru Java Core.  Partea 2 - 2Un proces și un fir sunt diferite în următoarele moduri:
  1. Un program care rulează se numește proces, dar un fir este o parte dintr-un proces.
  2. Procesele sunt independente, dar firele sunt părți ale unui proces.
  3. Procesele au spații de adrese diferite în memorie, dar firele de execuție au un spațiu de adrese comun.
  4. Comutarea contextului între fire este mai rapidă decât comutarea între procese.
  5. Comunicarea între procese este mai lentă și mai costisitoare decât comunicarea între fire.
  6. Orice modificare într-un proces părinte nu afectează un proces copil, dar modificările într-un fir părinte pot afecta un fir copil.

26. Care sunt beneficiile multithreading-ului?

  1. Multithreading permite unei aplicații/program să răspundă mereu la intrare, chiar dacă rulează deja unele sarcini de fundal;
  2. Multithreading face posibilă finalizarea sarcinilor mai rapid, deoarece firele rulează independent;
  3. Multithreading oferă o mai bună utilizare a memoriei cache, deoarece firele pot accesa resursele de memorie partajată;
  4. Multithreading reduce numărul de servere necesare, deoarece un server poate rula mai multe fire simultan.

27. Care sunt stările din ciclul de viață al unui fir?

Top 50 de întrebări și răspunsuri la interviul de angajare pentru Java Core.  Partea 2 - 3
  1. Nou: În această stare, obiectul Thread este creat folosind operatorul nou, dar un fir nou nu există încă. Firul nu începe până când nu apelăm metoda start() .
  2. Rulabil: în această stare, firul de execuție este gata să ruleze după start() se numeste metoda. Cu toate acestea, nu a fost încă selectat de către programatorul de fire.
  3. Rulează: în această stare, planificatorul de fire alege un fir dintr-o stare gata și rulează.
  4. În așteptare/Blocat: în această stare, un thread nu rulează, dar este încă viu sau așteaptă ca un alt thread să se finalizeze.
  5. Dead/Terminated: când un fir de execuție iese din metoda run() , acesta este într-o stare moartă sau terminată.

28. Este posibil să rulați un thread de două ori?

Nu, nu putem reporni un fir de execuție, deoarece după ce un fir de execuție începe și rulează, acesta intră în starea Dead. Dacă încercăm să începem un fir de execuție de două ori, va fi lansată o excepție java.lang.IllegalThreadStateException . Hai să aruncăm o privire:

class DoubleStartThreadExample extends Thread {

   /**
    * Simulate the work of a thread
    */
   public void run() {
	// Something happens. At this state, this is not essential.
   }

   /**
    * Start the thread twice
    */
   public static void main(String[] args) {
       DoubleStartThreadExample doubleStartThreadExample = new DoubleStartThreadExample();
       doubleStartThreadExample.start();
       doubleStartThreadExample.start();
   }
}
Va exista o excepție de îndată ce execuția vine la al doilea început al aceluiași fir. Încearcă singur ;) Este mai bine să vezi asta o dată decât să auzi de o sută de ori.

29. Ce se întâmplă dacă apelați direct run() fără a apela start()?

Da, cu siguranță puteți apela metoda run() , dar un fir nou nu va fi creat și metoda nu va rula pe un fir separat. În acest caz, avem un obiect obișnuit care cheamă o metodă obișnuită. Dacă vorbim despre metoda start() , atunci aceasta este o altă chestiune. Când această metodă este apelată, JVM- ul începe un nou thread. Acest thread, la rândul său, numește metoda noastră ;) Nu crezi? Iată, încearcă:

class ThreadCallRunExample extends Thread {

   public void run() {
       for (int i = 0; i < 5; i++) {
           System.out.print(i);
       }
   }

   public static void main(String args[]) {
       ThreadCallRunExample runExample1 = new ThreadCallRunExample();
       ThreadCallRunExample runExample2 = new ThreadCallRunExample();

       // Two ordinary methods will be called in the main thread, one after the other.
       runExample1.run();
       runExample2.run();
   }
}
Și ieșirea consolei va arăta astfel:
0123401234
După cum puteți vedea, nu a fost creat niciun thread. Totul a funcționat la fel ca într-o clasă obișnuită. Mai întâi a fost executată metoda primului obiect, apoi a doua.

30. Ce este un fir de demon?

Un fir de execuție demon este un fir de execuție care efectuează sarcini cu o prioritate mai mică decât un alt fir. Cu alte cuvinte, sarcina sa este de a efectua sarcini auxiliare care trebuie făcute numai împreună cu un alt fir (principal). Există multe fire de demoni care rulează automat, cum ar fi colectarea gunoiului, finalizatorul etc.

De ce Java termină un fir de demon?

Singurul scop al firului de demon este de a oferi suport de fundal firului de execuție al unui utilizator. În consecință, dacă firul de execuție principal este terminat, atunci JVM-ul termină automat toate firele sale de demon.

Metode din clasa Thread

Clasa java.lang.Thread oferă două metode de lucru cu un fir de demon:
  1. public void setDaemon(starea booleană) — Această metodă indică dacă acesta va fi un fir de demon. Valoarea implicită este false . Aceasta înseamnă că nu vor fi create fire de execuție daemon decât dacă spui în mod specific acest lucru.
  2. public boolean isDaemon() — Această metodă este în esență un getter pentru variabila demon , pe care o setăm folosind metoda anterioară.
Exemplu:

class DaemonThreadExample extends Thread {

   public void run() {
       // Checks whether this thread is a daemon
       if (Thread.currentThread().isDaemon()) {
           System.out.println("daemon thread");
       } else {
           System.out.println("user thread");
       }
   }

   public static void main(String[] args) {
       DaemonThreadExample thread1 = new DaemonThreadExample();
       DaemonThreadExample thread2 = new DaemonThreadExample();
       DaemonThreadExample thread3 = new DaemonThreadExample();

       // Make thread1 a daemon thread.
       thread1.setDaemon(true);

       System.out.println("daemon? " + thread1.isDaemon());
       System.out.println("daemon? " + thread2.isDaemon());
       System.out.println("daemon? " + thread3.isDaemon());

       thread1.start();
       thread2.start();
       thread3.start();
   }
}
Ieșire din consolă:
demon? demon adevărat? demon fals? fir de demon fals fir utilizator fir de utilizator
Din rezultat, vedem că în interiorul firului în sine, putem folosi metoda statică currentThread() pentru a afla ce fir este. Alternativ, dacă avem o referință la obiectul thread, putem afla și direct din acesta. Aceasta oferă nivelul necesar de configurabilitate.

31. Este posibil să faci dintr-un fir un demon după ce a fost creat?

Nu. Dacă încercați să faceți acest lucru, veți obține o excepție IllegalThreadStateException . Aceasta înseamnă că putem crea doar un fir de demon înainte ca acesta să înceapă. Exemplu:

class SetDaemonAfterStartExample extends Thread {

   public void run() {
       System.out.println("Working...");
   }

   public static void main(String[] args) {
       SetDaemonAfterStartExample afterStartExample = new SetDaemonAfterStartExample();
       afterStartExample.start();
      
       // An exception will be thrown here
       afterStartExample.setDaemon(true);
   }
}
Ieșire din consolă:
Funcționează... Excepție în firul „principal” java.lang.IllegalThreadStateException la java.lang.Thread.setDaemon(Thread.java:1359) la SetDaemonAfterStartExample.main(SetDaemonAfterStartExample.java:14)

32. Ce este un cârlig de închidere?

Un cârlig de închidere este un fir care este apelat implicit înainte ca mașina virtuală Java (JVM) să fie oprită. Astfel, îl putem folosi pentru a elibera o resursă sau a salva o stare atunci când mașina virtuală Java se oprește normal sau anormal. Putem adăuga un cârlig de închidere folosind următoarea metodă:

Runtime.getRuntime().addShutdownHook(new ShutdownHookThreadExample());
După cum se arată în exemplu:

/**
* A program that shows how to start a shutdown hook thread,
* which will be executed right before the JVM shuts down
*/
class ShutdownHookThreadExample extends Thread {

   public void run() {
       System.out.println("shutdown hook executed");
   }

   public static void main(String[] args) {

       Runtime.getRuntime().addShutdownHook(new ShutdownHookThreadExample());

       System.out.println("Now the program is going to fall asleep. Press Ctrl+C to terminate it.");
       try {
           Thread.sleep(60000);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
   }
}
Ieșire din consolă:
Acum programul va adormi. Apăsați Ctrl+C pentru a termina. cârlig de închidere executat

33. Ce este sincronizarea?

În Java, sincronizarea este capacitatea de a controla accesul mai multor fire de execuție la orice resursă partajată. Când mai multe fire de execuție încearcă să îndeplinească aceeași sarcină simultan, puteți obține un rezultat incorect. Pentru a remedia această problemă, Java folosește sincronizarea, care permite rularea unui singur fir odată. Sincronizarea poate fi realizată în trei moduri:
  • Sincronizarea unei metode
  • Sincronizarea unui anumit bloc
  • Sincronizare statică

Sincronizarea unei metode

O metodă sincronizată este utilizată pentru a bloca un obiect pentru orice resursă partajată. Când un fir apelează o metodă sincronizată, acesta dobândește automat blocarea obiectului și o eliberează când firul de execuție își finalizează sarcina. Pentru ca acest lucru să funcționeze, trebuie să adăugați cuvântul cheie sincronizat . Putem vedea cum funcționează acest lucru analizând un exemplu:

/**
* An example where we synchronize a method. That is, we add the synchronized keyword to it.
* There are two authors who want to use one printer. Each of them has composed their own poems
* And of course they don’t want their poems mixed up. Instead, they want work to be performed in * * * order for each of them
*/
class Printer {

   synchronized void print(List<String> wordsToPrint) {
       wordsToPrint.forEach(System.out::print);
       System.out.println();
   }

   public static void main(String args[]) {
       // One object for two threads
       Printer printer  = new Printer();

       // Create two threads
       Writer1 writer1 = new Writer1(printer);
       Writer2 writer2 = new Writer2(printer);

       // Start them
       writer1.start();
       writer2.start();
   }
}

/**
* Author No. 1, who writes an original poem.
*/
class Writer1 extends Thread {
   Printer printer;

   Writer1(Printer printer) {
       this.printer = printer;
   }

   public void run() {
       List<string> poem = Arrays.asList("I ", this.getName(), " Write", " A Letter");
       printer.print(poem);
   }

}

/**
* Author No. 2, who writes an original poem.
*/
class Writer2 extends Thread {
   Printer printer;

   Writer2(Printer printer) {
       this.printer = printer;
   }

   public void run() {
       List<String> poem = Arrays.asList("I Do Not ", this.getName(), " Not Write", " No Letter");
       printer.print(poem);
   }
}
Și ieșirea consolei este aceasta:
Eu Thread-0 Scrie o scrisoare Nu Thread-1 Nu scrie nicio scrisoare

Bloc de sincronizare

Un bloc sincronizat poate fi utilizat pentru a efectua sincronizarea pe orice resursă particulară dintr-o metodă. Să spunem că într-o metodă mare (da, nu ar trebui să le scrieți, dar uneori se întâmplă) trebuie să sincronizați doar o mică secțiune dintr-un motiv oarecare. Dacă puneți tot codul metodei într-un bloc sincronizat, acesta va funcționa la fel ca o metodă sincronizată. Sintaxa arată astfel:

synchronized ("object to be locked") {
   // The code that must be protected
}
Pentru a evita repetarea exemplului anterior, vom crea fire folosind clase anonime, adică vom implementa imediat interfața Runnable.

/**
* This is how a synchronization block is added.
* Inside the block, you need to specify which object's mutex will be acquired.
*/
class Printer {

   void print(List<String> wordsToPrint) {
       synchronized (this) {
           wordsToPrint.forEach(System.out::print);
       }
       System.out.println();
   }

   public static void main(String args[]) {
       // One object for two threads
       Printer printer = new Printer();

       // Create two threads
       Thread writer1 = new Thread(new Runnable() {
           @Override
           public void run() {
               List<String> poem = Arrays.asList("I ", "Writer1", " Write", " A Letter");
               printer.print(poem);
           }
       });
       Thread writer2 = new Thread(new Runnable() {
           @Override
           public void run() {
               List<String> poem = Arrays.asList("I Do Not ", "Writer2", " Not Write", " No Letter");
               printer.print(poem);
           }
       });

       // Start them
       writer1.start();
       writer2.start();
   }
}

}
Și ieșirea consolei este aceasta:
Eu scriitor1 Scrie o scrisoare Eu nu scriu2 Nu scriu nicio scrisoare

Sincronizare statică

Dacă faci o metodă statică sincronizată, atunci blocarea se va întâmpla pe clasă, nu pe obiect. În acest exemplu, efectuăm sincronizarea statică prin aplicarea cuvântului cheie sincronizat la o metodă statică:

/**
* This is how a synchronization block is added.
* Inside the block, you need to specify which object's mutex will be acquired.
*/
class Printer {

   static synchronized void print(List<String> wordsToPrint) {
       wordsToPrint.forEach(System.out::print);
       System.out.println();
   }

   public static void main(String args[]) {

       // Create two threads
       Thread writer1 = new Thread(new Runnable() {
           @Override
           public void run() {
               List<String> poem = Arrays.asList("I ", "Writer1", " Write", " A Letter");
               Printer.print(poem);
           }
       });
       Thread writer2 = new Thread(new Runnable() {
           @Override
           public void run() {
               List<String> poem = Arrays.asList("I Do Not ", "Writer2", " Not Write", " No Letter");
               Printer.print(poem);
           }
       });

       // Start them
       writer1.start();
       writer2.start();
   }
}
Și ieșirea consolei este aceasta:
Eu nu scriu2 nu scriu nicio scrisoare I scriitorul1 scriu o scrisoare

34. Ce este o variabilă volatilă?

În programarea cu mai multe fire, cuvântul cheie volatil este folosit pentru siguranța firelor. Când o variabilă mutabilă este modificată, modificarea este vizibilă pentru toate celelalte fire de execuție, astfel încât o variabilă poate fi utilizată de un fir de execuție la un moment dat. Folosind cuvântul cheie volatil , puteți garanta că o variabilă este sigură pentru fire și este stocată în memoria partajată și că firele de execuție nu o vor stoca în memoria cache. Cum arată asta?

private volatile AtomicInteger count;
Adăugăm doar volatil la variabilă. Dar rețineți că acest lucru nu înseamnă siguranță completă a firului... La urma urmei, operațiunile asupra variabilei pot să nu fie atomice. Acestea fiind spuse, puteți folosi clase Atomic care fac operații atomic, adică într-o singură instrucțiune CPU. Există multe astfel de clase în pachetul java.util.concurrent.atomic .

35. Ce este blocajul?

În Java, blocajul este ceva care se poate întâmpla ca parte a multithreading-ului. Un blocaj poate apărea atunci când un fir așteaptă blocarea unui obiect dobândită de un alt fir, iar al doilea thread așteaptă blocarea obiectului dobândit de primul fir. Aceasta înseamnă că cele două fire se așteaptă unul pe celălalt, iar execuția codului lor nu poate continua. Top 50 de întrebări și răspunsuri la interviul de angajare pentru Java Core.  Partea 2 - 4Să luăm în considerare un exemplu care are o clasă care implementează Runnable. Constructorul său necesită două resurse. Metoda run() dobândește blocarea pentru ei în ordine. Dacă creați două obiecte din această clasă și treceți resursele într-o ordine diferită, atunci puteți intra cu ușurință în impas:

class DeadLock {

   public static void main(String[] args) {
       final Integer r1 = 10;
       final Integer r2 = 15;

       DeadlockThread threadR1R2 = new DeadlockThread(r1, r2);
       DeadlockThread threadR2R1 = new DeadlockThread(r2, r1);

       new Thread(threadR1R2).start();
       new Thread(threadR2R1).start();
   }
}

/**
* A class that accepts two resources.
*/
class DeadlockThread implements Runnable {

   private final Integer r1;
   private final Integer r2;

   public DeadlockThread(Integer r1, Integer r2) {
       this.r1 = r1;
       this.r2 = r2;
   }

   @Override
   public void run() {
       synchronized (r1) {
           System.out.println(Thread.currentThread().getName() + " acquired resource: " + r1);

           try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }

           synchronized (r2) {
               System.out.println(Thread.currentThread().getName() + " acquired resource: " + r2);
           }
       }
   }
}
Ieșire din consolă:
Primul thread a dobândit prima resursă Al doilea thread a dobândit a doua resursă

36. Cum eviți blocajul?

Pentru că știm cum se produce blocajul, putem trage câteva concluzii...
  • În exemplul de mai sus, blocajul apare din cauza faptului că avem blocare imbricată. Adică avem un bloc sincronizat în interiorul unui bloc sincronizat. Pentru a evita acest lucru, în loc de imbricare, trebuie să creați un nou strat de abstractizare superior, să mutați sincronizarea la nivelul superior și să eliminați blocarea imbricată.
  • Cu cât blocați mai mult, cu atât este mai probabil să existe un impas. Prin urmare, de fiecare dată când adăugați un bloc sincronizat, trebuie să vă gândiți dacă aveți cu adevărat nevoie de el și dacă puteți evita adăugarea unuia nou.
  • Folosind Thread.join() . De asemenea, puteți intra în impas în timp ce un fir așteaptă altul. Pentru a evita această problemă, ați putea lua în considerare setarea unui timeout pentru metoda join() .
  • Dacă avem un fir, atunci nu va exista niciun blocaj ;)

37. Ce este o condiție de rasă?

Dacă cursele din viața reală implică mașini, atunci cursele în multithreading implică fire. Dar de ce? :/ Există două fire care rulează și pot accesa același obiect. Și pot încerca să actualizeze starea obiectului partajat în același timp. Totul este clar până acum, nu? Thread-urile sunt executate fie literal în paralel (dacă procesorul are mai multe nuclee), fie secvenţial, procesorul alocand secţiuni de timp intercalate. Nu putem gestiona aceste procese. Aceasta înseamnă că, atunci când un fir citește date de la un obiect, nu putem garanta că va avea timp să schimbe obiectul ÎNAINTE să facă un alt thread. Astfel de probleme apar atunci când avem aceste combo-uri „verificați și acționați”. Ce înseamnă asta? Să presupunem că avem o declarație if al cărei corp schimbă condiția dacă în sine, de exemplu:

int z = 0;

// Check
if (z < 5) {
// Act
   z = z + 5;
}
Două fire de execuție ar putea intra simultan în acest bloc de cod atunci când z este încă zero și apoi ambele fire de execuție își pot schimba valoarea. Ca rezultat, nu vom obține valoarea așteptată de 5. În schimb, am obține 10. Cum eviți acest lucru? Trebuie să obțineți un blocare înainte de a verifica și de a acționa, apoi eliberați blocarea după aceea. Adică, trebuie să aveți primul fir de execuție să intre în blocul if , să efectuați toate acțiunile, să schimbați z și abia apoi să oferiți următorului fir posibilitatea de a face același lucru. Dar următorul thread nu va intra în blocul if , deoarece z va fi acum 5:

// Acquire the lock for z
if (z < 5) {
   z = z + 5;
}
// Release z's lock
===================================================

În loc de concluzie

Vreau să le mulțumesc tuturor celor care au citit până la sfârșit. A fost drum lung, dar ai îndurat! Poate că nu totul este clar. Asta este normal. Când am început să studiez Java, nu mi-am putut învălui ce este o variabilă statică. Dar nu mare lucru. Am dormit pe el, am mai citit câteva surse și apoi a venit înțelegerea. Pregătirea pentru un interviu este mai degrabă o întrebare academică decât una practică. Ca urmare, înainte de fiecare interviu, ar trebui să revizuiți și să vă reîmprospătați în memorie acele lucruri pe care este posibil să nu le folosiți foarte des.

Și, ca întotdeauna, iată câteva link-uri utile:

Vă mulțumesc tuturor pentru lectura. Ne vedem curând :) Profilul meu GitHubTop 50 de întrebări și răspunsuri la interviul de angajare pentru Java Core.  Partea 2 - 5
Comentarii
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION