CodeGym /Java Blog /Random-IT /Le 50 principali domande e risposte sui colloqui di lavor...
John Squirrels
Livello 41
San Francisco

Le 50 principali domande e risposte sui colloqui di lavoro per Java Core. Parte 2

Pubblicato nel gruppo Random-IT
Le 50 principali domande e risposte sui colloqui di lavoro per Java Core. Parte 1Le 50 principali domande e risposte sui colloqui di lavoro per Java Core.  Parte 2 - 1

Multithreading

24. Come posso creare un nuovo thread in Java?

In un modo o nell'altro, un thread viene creato utilizzando la classe Thread. Ma ci sono vari modi per farlo...
  1. Eredita java.lang.Thread .
  2. Implementa l' interfaccia java.lang.Runnable : il costruttore della classe Thread accetta un oggetto Runnable.
Parliamo di ognuno di loro.

Eredita la classe Thread

In questo caso, ereditiamo la nostra classe java.lang.Thread . Ha un metodo run() , ed è proprio quello di cui abbiamo bisogno. Tutta la vita e la logica del nuovo thread saranno in questo metodo. È una specie di metodo principale per il nuovo thread. Dopodiché, non resta che creare un oggetto della nostra classe e chiamare il metodo start() . Questo creerà un nuovo thread e inizierà a eseguire la sua logica. Diamo un'occhiata:

/**
* 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();
   }
}
L'output della console sarà qualcosa del genere:
Filo-1 Filo-0 Filo-2
Cioè, anche qui vediamo che i thread vengono eseguiti non in ordine, ma piuttosto come la JVM ritiene opportuno eseguirli :)

Implementare l'interfaccia Runnable

Se sei contrario all'ereditarietà e/o erediti già un'altra classe, puoi utilizzare l' interfaccia java.lang.Runnable . Qui facciamo in modo che la nostra classe implementi questa interfaccia implementando il metodo run() , proprio come nell'esempio precedente. Non resta che creare oggetti Thread . Sembrerebbe che più righe di codice siano peggio. Ma sappiamo quanto sia dannosa l'ereditarietà e che è meglio evitarla a tutti i costi ;) Dai un'occhiata:

/**
* 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();
   }
}
Ed ecco il risultato:
Filo-0 Filo-1 Filo-2

25. Qual è la differenza tra un processo e un thread?

Le 50 principali domande e risposte sui colloqui di lavoro per Java Core.  Parte 2 - 2Un processo e un thread sono diversi nei seguenti modi:
  1. Un programma in esecuzione è chiamato processo, ma un thread è una parte di un processo.
  2. I processi sono indipendenti, ma i thread sono parti di un processo.
  3. I processi hanno diversi spazi di indirizzi in memoria, ma i thread condividono uno spazio di indirizzi comune.
  4. Il cambio di contesto tra i thread è più veloce del passaggio tra i processi.
  5. La comunicazione tra processi è più lenta e più costosa della comunicazione tra thread.
  6. Eventuali modifiche in un processo padre non influiscono su un processo figlio, ma le modifiche in un thread padre possono influire su un thread figlio.

26. Quali sono i vantaggi del multithreading?

  1. Il multithreading consente a un'applicazione/programma di rispondere sempre all'input, anche se sta già eseguendo alcune attività in background;
  2. Il multithreading consente di completare le attività più velocemente, poiché i thread vengono eseguiti in modo indipendente;
  3. Il multithreading fornisce un uso migliore della memoria cache, poiché i thread possono accedere alle risorse di memoria condivise;
  4. Il multithreading riduce il numero di server richiesti, poiché un server può eseguire più thread contemporaneamente.

27. Quali sono gli stati nel ciclo di vita di un thread?

Le 50 principali domande e risposte sui colloqui di lavoro per Java Core.  Parte 2 - 3
  1. Nuovo: in questo stato, l'oggetto Thread viene creato utilizzando l'operatore new, ma non esiste ancora un nuovo thread. Il thread non si avvia finché non chiamiamo il metodo start() .
  2. Eseguibile: in questo stato, il thread è pronto per essere eseguito dopo start() metodo è chiamato. Tuttavia, non è stato ancora selezionato dallo scheduler dei thread.
  3. In esecuzione: in questo stato, l'utilità di pianificazione del thread seleziona un thread da uno stato pronto e viene eseguito.
  4. In attesa/Bloccato: in questo stato, un thread non è in esecuzione, ma è ancora attivo o in attesa del completamento di un altro thread.
  5. Inattivo/terminato: quando un thread esce dal metodo run() , si trova in uno stato inattivo o terminato.

28. È possibile eseguire un thread due volte?

No, non possiamo riavviare un thread, perché dopo l'avvio e l'esecuzione di un thread, passa allo stato Dead. Se proviamo ad avviare un thread due volte, verrà generata un'eccezione java.lang.IllegalThreadStateException . Diamo un'occhiata:

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();
   }
}
Ci sarà un'eccezione non appena l'esecuzione arriva al secondo inizio dello stesso thread. Provalo tu stesso ;) È meglio vederlo una volta che sentirne parlare cento volte.

29. Cosa succede se chiami run() direttamente senza chiamare start()?

Sì, puoi sicuramente chiamare il metodo run() , ma non verrà creato un nuovo thread e il metodo non verrà eseguito su un thread separato. In questo caso, abbiamo un oggetto ordinario che chiama un metodo ordinario. Se stiamo parlando del metodo start() , questa è un'altra questione. Quando viene chiamato questo metodo, la JVM avvia un nuovo thread. Questo thread, a sua volta, chiama il nostro metodo ;) Non ci credi? Ecco, provaci:

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();
   }
}
E l'output della console sarà simile a questo:
0123401234
Come puoi vedere, non è stato creato alcun thread. Tutto ha funzionato proprio come in una normale classe. Innanzitutto, è stato eseguito il metodo del primo oggetto, quindi il secondo.

30. Cos'è un thread demone?

Un thread daemon è un thread che esegue attività con una priorità inferiore rispetto a un altro thread. In altre parole, il suo compito è eseguire attività ausiliarie che devono essere eseguite solo in combinazione con un altro thread (principale). Esistono molti thread daemon che vengono eseguiti automaticamente, come la raccolta dei rifiuti, il finalizzatore, ecc.

Perché Java termina un thread demone?

L'unico scopo del thread daemon è fornire supporto in background al thread di un utente. Di conseguenza, se il thread principale viene terminato, la JVM termina automaticamente tutti i suoi thread daemon.

Metodi della classe Thread

La classe java.lang.Thread fornisce due metodi per lavorare con un thread daemon:
  1. public void setDaemon(boolean status) — Questo metodo indica se questo sarà un thread demone. L'impostazione predefinita è false . Ciò significa che non verranno creati thread daemon a meno che tu non lo dica espressamente.
  2. public boolean isDaemon() — Questo metodo è essenzialmente un getter per la variabile demone , che abbiamo impostato utilizzando il metodo precedente.
Esempio:

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();
   }
}
Uscita console:
demone? vero demone? falso demone? false thread daemon thread utente thread utente
Dall'output, vediamo che all'interno del thread stesso, possiamo usare il metodo statico currentThread() per scoprire di quale thread si tratta. In alternativa, se abbiamo un riferimento all'oggetto thread, possiamo anche scoprirlo direttamente da esso. Ciò fornisce il necessario livello di configurabilità.

31. È possibile trasformare un thread in un demone dopo che è stato creato?

No. Se provi a farlo, otterrai un'eccezione IllegalThreadStateException . Ciò significa che possiamo creare un thread demone solo prima che inizi. Esempio:

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);
   }
}
Uscita console:
Funzionante... Eccezione nel thread "principale" java.lang.IllegalThreadStateException in java.lang.Thread.setDaemon(Thread.java:1359) in SetDaemonAfterStartExample.main(SetDaemonAfterStartExample.java:14)

32. Che cos'è un gancio di spegnimento?

Un hook di arresto è un thread che viene richiamato in modo implicito prima che la JVM (Java virtual machine) venga arrestata. Pertanto, possiamo usarlo per rilasciare una risorsa o salvare lo stato quando la macchina virtuale Java si spegne normalmente o in modo anomalo. Possiamo aggiungere un hook di spegnimento usando il seguente metodo:

Runtime.getRuntime().addShutdownHook(new ShutdownHookThreadExample());
Come mostrato nell'esempio:

/**
* 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();
       }
   }
}
Uscita console:
Ora il programma si addormenterà. Premi Ctrl+C per terminarlo. hook di arresto eseguito

33. Cos'è la sincronizzazione?

In Java, la sincronizzazione è la capacità di controllare l'accesso di più thread a qualsiasi risorsa condivisa. Quando più thread tentano di eseguire la stessa attività contemporaneamente, potresti ottenere un risultato errato. Per risolvere questo problema, Java utilizza la sincronizzazione, che consente l'esecuzione di un solo thread alla volta. La sincronizzazione può essere ottenuta in tre modi:
  • Sincronizzazione di un metodo
  • Sincronizzazione di un blocco specifico
  • Sincronizzazione statica

Sincronizzazione di un metodo

Un metodo sincronizzato viene utilizzato per bloccare un oggetto per qualsiasi risorsa condivisa. Quando un thread chiama un metodo sincronizzato, acquisisce automaticamente il blocco dell'oggetto e lo rilascia quando il thread completa la sua attività. Per fare in modo che funzioni, devi aggiungere la parola chiave sincronizzata . Possiamo vedere come funziona guardando un esempio:

/**
* 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);
   }
}
E l'output della console è questo:
I Thread-0 Scrivi una lettera Non ho Thread-1 Non scrivere nessuna lettera

Blocco di sincronizzazione

Un blocco sincronizzato può essere utilizzato per eseguire la sincronizzazione su qualsiasi particolare risorsa in un metodo. Diciamo che in un metodo di grandi dimensioni (sì, non dovresti scriverli, ma a volte accadono) devi sincronizzare solo una piccola sezione per qualche motivo. Se metti tutto il codice del metodo in un blocco sincronizzato, funzionerà come un metodo sincronizzato. La sintassi è simile a questa:

synchronized ("object to be locked") {
   // The code that must be protected
}
Per evitare di ripetere l'esempio precedente, creeremo thread utilizzando classi anonime, cioè implementeremo immediatamente l'interfaccia 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();
   }
}

}
E l'output della console è questo:
I Writer1 Scrivi una lettera Non Writer2 Non scrivo nessuna lettera

Sincronizzazione statica

Se rendi sincronizzato un metodo statico, il blocco avverrà sulla classe, non sull'oggetto. In questo esempio, eseguiamo la sincronizzazione statica applicando la parola chiavesynchronized a un metodo statico:

/**
* 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();
   }
}
E l'output della console è questo:
Non scrivo2 Non scrivo nessuna lettera I Scrittore1 Scrivi una lettera

34. Cos'è una variabile volatile?

Nella programmazione multithread, la parola chiave volatile viene utilizzata per la sicurezza dei thread. Quando una variabile mutabile viene modificata, la modifica è visibile a tutti gli altri thread, quindi una variabile può essere utilizzata da un thread alla volta. Utilizzando la volatile parola chiave, puoi garantire che una variabile sia thread-safe e archiviata nella memoria condivisa e che i thread non la memorizzeranno nelle loro cache. Cosa sembra questo?

private volatile AtomicInteger count;
Aggiungiamo semplicemente volatile alla variabile. Ma tieni presente che questo non significa completa sicurezza del thread... Dopotutto, le operazioni sulla variabile potrebbero non essere atomiche. Detto questo, puoi usare le classi Atomic che eseguono operazioni in modo atomico, cioè in una singola istruzione della CPU. Ci sono molte di queste classi nel pacchetto java.util.concurrent.atomic .

35. Cos'è lo stallo?

In Java, il deadlock è qualcosa che può verificarsi come parte del multithreading. Un deadlock può verificarsi quando un thread è in attesa del blocco di un oggetto acquisito da un altro thread e il secondo thread è in attesa del blocco dell'oggetto acquisito dal primo thread. Ciò significa che i due thread sono in attesa l'uno dell'altro e l'esecuzione del loro codice non può continuare. Le 50 principali domande e risposte sui colloqui di lavoro per Java Core.  Parte 2 - 4Consideriamo un esempio che ha una classe che implementa Runnable. Il suo costruttore prende due risorse. Il metodo run() acquisisce il blocco per loro in ordine. Se crei due oggetti di questa classe e passi le risorse in un ordine diverso, puoi facilmente imbatterti in un deadlock:

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);
           }
       }
   }
}
Uscita console:
Il primo thread ha acquisito la prima risorsa Il secondo thread ha acquisito la seconda risorsa

36. Come si evita lo stallo?

Poiché sappiamo come si verifica lo stallo, possiamo trarre alcune conclusioni...
  • Nell'esempio sopra, il deadlock si verifica a causa del fatto che abbiamo un lock annidato. Cioè, abbiamo un blocco sincronizzato all'interno di un blocco sincronizzato. Per evitare ciò, invece di nidificare, è necessario creare un nuovo livello di astrazione superiore, spostare la sincronizzazione al livello superiore ed eliminare il blocco nidificato.
  • Più blocchi fai, più è probabile che ci sia un deadlock. Pertanto, ogni volta che aggiungi un blocco sincronizzato, devi pensare se ne hai davvero bisogno e se puoi evitare di aggiungerne uno nuovo.
  • Utilizzo di Thread.join() . Puoi anche imbatterti in un deadlock mentre un thread attende un altro. Per evitare questo problema, potresti prendere in considerazione l'impostazione di un timeout per il metodo join() .
  • Se abbiamo un thread, non ci sarà nessun deadlock ;)

37. Cos'è una race condition?

Se le gare nella vita reale coinvolgono le auto, le gare nel multithreading coinvolgono i thread. Ma perché? :/ Ci sono due thread in esecuzione che possono accedere allo stesso oggetto. E possono tentare di aggiornare lo stato dell'oggetto condiviso allo stesso tempo. È tutto chiaro finora, vero? I thread vengono eseguiti letteralmente in parallelo (se il processore ha più di un core) o in sequenza, con il processore che alloca intervalli di tempo interlacciati. Non possiamo gestire questi processi. Ciò significa che quando un thread legge i dati da un oggetto non possiamo garantire che avrà il tempo di modificare l'oggetto PRIMA che lo faccia un altro thread. Tali problemi sorgono quando abbiamo queste combo "controlla e agisci". Che cosa significa? Supponiamo di avere un'istruzione if il cui corpo modifica la condizione if stessa, ad esempio:

int z = 0;

// Check
if (z < 5) {
// Act
   z = z + 5;
}
Due thread potrebbero entrare contemporaneamente in questo blocco di codice quando z è ancora zero e quindi entrambi i thread potrebbero modificarne il valore. Di conseguenza, non otterremo il valore atteso di 5. Invece, otterremo 10. Come si evita questo? È necessario acquisire un blocco prima di controllare e agire, quindi rilasciare il blocco in seguito. Cioè, è necessario che il primo thread entri nel blocco if , esegua tutte le azioni, cambi z e solo allora dia al thread successivo l'opportunità di fare lo stesso. Ma il thread successivo non entrerà nel blocco if , poiché z sarà ora 5:

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

Invece di una conclusione

Voglio ringraziare tutti coloro che hanno letto fino alla fine. È stata una lunga strada, ma hai resistito! Forse non tutto è chiaro. E 'normale. Quando ho iniziato a studiare Java, non riuscivo a pensare a cosa fosse una variabile statica. Ma niente di grave. Ci ho dormito sopra, ho letto qualche altra fonte e poi è arrivata la comprensione. La preparazione per un colloquio è più una questione accademica che pratica. Di conseguenza, prima di ogni colloquio, dovresti rivedere e rinfrescare nella tua memoria quelle cose che potresti non usare molto spesso.

E come sempre, ecco alcuni link utili:

Grazie a tutti per la lettura. A presto :) Il mio profilo GitHubLe 50 principali domande e risposte sui colloqui di lavoro per Java Core.  Parte 2 - 5
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION