JavaRush /Java Blog /Random-IT /Le 50 migliori domande e risposte per l'intervista su Jav...
Roman Beekeeper
Livello 35

Le 50 migliori domande e risposte per l'intervista su Java Core. Parte 3

Pubblicato nel gruppo Random-IT
Le 50 migliori domande e risposte per l'intervista su Java Core. Parte 1 Le 50 principali domande e risposte dell'intervista Java Core. Parte 2

Multithreading

37. Come creare un nuovo thread (flusso) in Java?

In un modo o nell'altro, la creazione avviene tramite l'utilizzo della classe Thread. Ma qui potrebbero esserci delle opzioni...
  1. Ereditiamo dajava.lang.Thread
  2. Implementiamo un'interfaccia il cui oggetto accetta una classe java.lang.RunnablecostruttoreThread
Parliamo di ciascuno di essi.

Ereditiamo dalla classe Thread

Per far sì che funzioni, nella nostra classe ereditiamo da java.lang.Thread. Contiene metanfetamina run(), che è esattamente ciò di cui abbiamo bisogno. Tutta la vita e la logica del nuovo thread saranno in questo metodo. Questo è un tipo di mainmetodo per un nuovo thread. Fatto questo non resta che creare un oggetto della nostra classe ed eseguire il metodo start(), che creerà un nuovo thread ed eseguirà la logica scritta in esso. Guardiamo:
/**
* Пример того, How создавать треды путем наследования {@link Thread} класса.
*/
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 sulla console sarà così:

Thread-1
Thread-0
Thread-2
Cioè, anche qui vediamo che i thread non vengono eseguiti uno dopo l'altro, ma come deciso dalla JVM)

Implementazione dell'interfaccia Runnable

Se sei contrario all'ereditarietà e/o erediti già una delle altre classi, puoi utilizzare java.lang.Runnable. Qui nella nostra classe implementiamo questa interfaccia e implementiamo il metodo run(), come in quell'esempio. Hai solo bisogno di creare più oggetti Thread. Sembrerebbe che più linee siano peggiori. Ma sappiamo quanto sia dannosa l'ereditarietà e che è meglio evitarla in ogni caso ;) Vediamo:
/**
* Пример того, How создавать треды из интерфейса {@link Runnable}.
* Здесь проще простого - реализуем этот интерфейс и потом передаем в конструктор
* экземпляр реализуемого an object.
*/
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();
   }
}
E il risultato dell'esecuzione:

Thread-0
Thread-1
Thread-2

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

Le 50 migliori domande e risposte per l'intervista su Java Core.  Parte 3 - 1Esistono le seguenti differenze tra un processo e un thread:
  1. Un programma in esecuzione è chiamato processo, mentre un thread è un sottoinsieme di un processo.
  2. I processi sono indipendenti, mentre i thread sono un sottoinsieme di un processo.
  3. I processi hanno spazi di indirizzi diversi in memoria, mentre i thread contengono uno spazio di indirizzi comune.
  4. Il cambio di contesto è più veloce tra i thread rispetto ai processi.
  5. La comunicazione tra processi è più lenta e più costosa della comunicazione tra thread.
  6. Eventuali modifiche nel processo padre non influiscono sul processo figlio, mentre le modifiche nel thread padre possono influenzare il thread figlio.

39. Quali sono i vantaggi del multithreading?

Le 50 migliori domande e risposte per l'intervista su Java Core.  Parte 3 - 2
  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 ti consente di completare le attività più velocemente perché i thread vengono eseguiti in modo indipendente;
  3. Il multithreading fornisce un migliore utilizzo della cache perché i thread condividono risorse di memoria comuni;
  4. Il multithreading riduce la quantità di server richiesta perché un server può eseguire più thread contemporaneamente.

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

Le 50 migliori domande e risposte per l'intervista su Java Core.  Parte 3 - 3
  1. Nuovo: in questo stato, Threadviene creato un oggetto classe utilizzando l'operatore new, ma il thread non esiste. Il thread non inizia finché non chiamiamo il file start().
  2. Eseguibile: in questo stato, il thread è pronto per essere eseguito dopo aver chiamato il metodo inizio(). Tuttavia, non è stato ancora selezionato dallo scheduler dei thread.
  3. In esecuzione: in questo stato, lo scheduler del thread seleziona un thread dallo stato pronto e viene eseguito.
  4. In attesa/Bloccato: in questo stato, il thread non è in esecuzione ma è ancora attivo o in attesa del completamento di un altro thread.
  5. Inattivo/terminato: quando il metodo termina, run()il thread è in uno stato terminato o inattivo.

41. È possibile iniziare una discussione due volte?

No, non possiamo riavviare il thread perché una volta avviato ed eseguito il thread entra nello stato Dead. Quindi, se proviamo a eseguire il thread due volte, verrà lanciata una runtimeException " java.lang.IllegalThreadStateException ". Guardiamo:
class DoubleStartThreadExample extends Thread {

   /**
    * Имитируем работу треда
    */
   public void run() {
	// что-то происходит. Для нас не существенно на этом этапе
   }

   /**
    * Запускаем тред дважды
    */
   public static void main(String[] args) {
       DoubleStartThreadExample doubleStartThreadExample = new DoubleStartThreadExample();
       doubleStartThreadExample.start();
       doubleStartThreadExample.start();
   }
}
Non appena il lavoro raggiunge il secondo inizio dello stesso thread, si verificherà un'eccezione. Provalo tu stesso ;) è meglio vedere una volta che ascoltare cento volte.

42. Cosa succede se chiami direttamente il metodo run() senza chiamare il metodo start()?

Sì, run()ovviamente puoi chiamare un metodo, ma questo non creerà un nuovo thread e non lo eseguirà come thread separato. In questo caso si tratta di un oggetto semplice che chiama un metodo semplice. Se parliamo di metodo start()allora la questione è diversa. Lanciando questo metodo, runtimene viene lanciato uno nuovo che, a sua volta, esegue il nostro metodo ;) Se non mi credi, provalo:
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();

       // просто будут вызваны в потоке main два метода, один за другим.
       runExample1.run();
       runExample2.run();
   }
}
E l'output sulla console sarà così:

0123401234
Si può vedere che non è stato creato alcun thread. Tutto ha funzionato come una lezione normale. Prima ha funzionato il metodo della prima classe, poi il secondo.

43. Cos'è un thread demone?

Le 50 migliori domande e risposte per l'intervista su Java Core.  Parte 3 - 4Il thread demone (di seguito denominato thread demone) è un thread che esegue attività in background in relazione a un altro thread. Cioè, il suo compito è eseguire attività ausiliarie che devono essere eseguite solo insieme a un altro thread (principale). Esistono molti thread di demoni che funzionano automaticamente, come Garbage Collector, finalizzatore, ecc.

Perché Java chiude il thread del demone?

L'unico scopo di un thread daemon è fornire servizi al thread utente per attività di supporto in background. Pertanto, se il thread principale è stato completato, il runtime chiude automaticamente tutti i thread del daemon.

Metodi per lavorare nella classe Thread

La classe java.lang.Threadfornisce due metodi per lavorare con il demone thread:
  1. public void setDaemon(boolean status)- indica che questo sarà un thread demone. Il valore predefinito è false, il che significa che verranno creati thread non demoniaci a meno che non sia specificato separatamente.
  2. public boolean isDaemon()- essenzialmente questo è un getter per la variabile daemonche abbiamo impostato utilizzando il metodo precedente.
Esempio:
class DaemonThreadExample extends Thread {

   public void run() {
       // Проверяет, демон ли этот поток or нет
       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();

       // теперь thread1 - поток-демон.
       thread1.setDaemon(true);

       System.out.println("демон?.. " + thread1.isDaemon());
       System.out.println("демон?.. " + thread2.isDaemon());
       System.out.println("демон?.. " + thread3.isDaemon());

       thread1.start();
       thread2.start();
       thread3.start();
   }
}
Uscita console:

демон?.. true
демон?.. false
демон?.. false
daemon thread
user thread
user thread
Dall'output vediamo che all'interno del thread stesso, utilizzando un currentThread()metodo statico, possiamo scoprire da un lato di quale thread si tratta, dall'altro, se abbiamo un riferimento all'oggetto di questo thread, possiamo scoprire direttamente da esso. Ciò offre la flessibilità necessaria nella configurazione.

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

NO. Se lo fai, genererà un'eccezione IllegalThreadStateException. Pertanto, possiamo solo creare un thread demone 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();

       // здесь будет выброшено исключение
       afterStartExample.setDaemon(true);
   }
}
Uscita console:

Working...
Exception in thread "main" java.lang.IllegalThreadStateException
	at java.lang.Thread.setDaemon(Thread.java:1359)
	at SetDaemonAfterStartExample.main(SetDaemonAfterStartExample.java:14)

45. Cos'è un gancio di arresto?

Shutdownhook è un thread che viene chiamato implicitamente prima dello spegnimento della JVM (Java Virtual Machine). Quindi possiamo usarlo per ripulire una risorsa o salvare lo stato quando la Java Virtual Machine si spegne normalmente o improvvisamente. Possiamo aggiungere shutdown hookutilizzando il seguente metodo:
Runtime.getRuntime().addShutdownHook(new ShutdownHookThreadExample());
Come mostrato nell'esempio:
/**
* Программа, которая показывает How запустить shutdown hook тред,
* который выполнится аккурат до окончания работы JVM
*/
class ShutdownHookThreadExample extends Thread {

   public void run() {
       System.out.println("shutdown hook задачу выполнил");
   }

   public static void main(String[] args) {

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

       System.out.println("Теперь программа засыпает, нажмите ctrl+c чтоб завершить ее.");
       try {
           Thread.sleep(60000);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
   }
}
Uscita console:

Теперь программа засыпает, нажмите ctrl+c чтоб завершить ее.
shutdown hook задачу выполнил

46. ​​​​Che cos'è la sincronizzazione?

La sincronizzazione in Java è la capacità di controllare l'accesso di più thread a qualsiasi risorsa condivisa. Quando più thread tentano di eseguire la stessa attività, esiste la possibilità di un risultato errato, quindi per superare questo problema, Java utilizza la sincronizzazione, grazie alla quale può funzionare solo un thread alla volta. La sincronizzazione può essere ottenuta in tre modi:
  • Metodo di sincronizzazione
  • Sincronizzando un blocco specifico
  • Sincronizzazione statica

Sincronizzazione dei metodi

Il metodo sincronizzato viene utilizzato per bloccare un oggetto per qualsiasi risorsa condivisa. Quando un thread chiama un metodo sincronizzato, acquisisce automaticamente un blocco su quell'oggetto e lo rilascia quando il thread completa la sua attività. Per farlo funzionare, è necessario aggiungere la parola chiave sincronizzata . Vediamo come funziona con un esempio:
/**
* Пример, где мы синхронизируем метод. То есть добавляем ему слово synchronized.
* Есть два писателя, которые хотят использовать один принтер. Они подготовor свои поэмы
* И конечно же не хотят, чтоб их поэмы перемешались, а хотят, чтоб работа была сделана по * * * очереди для каждого из них
*/
class Printer {

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

   public static void main(String args[]) {
       // один an object для двух тредов
       Printer printer  = new Printer();

       // создаем два треда
       Writer1 writer1 = new Writer1(printer);
       Writer2 writer2 = new Writer2(printer);

       // запускаем их
       writer1.start();
       writer2.start();
   }
}

/**
* Писатель номер 1, который пишет свою поэму.
*/
class Writer1 extends Thread {
   Printer printer;

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

   public void run() {
       List<string> poem = Arrays.asList("Я ", this.getName(), " Пишу", " Письмо");
       printer.print(poem);
   }

}

/**
* Писатель номер 2, который пишет свою поэму.
*/
class Writer2 extends Thread {
   Printer printer;

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

   public void run() {
       List<String> poem = Arrays.asList("Не Я ", this.getName(), " Не пишу", " Не Письмо");
       printer.print(poem);
   }
}
E l'output sulla console:

Я Thread-0 Пишу Письмо
Не Я Thread-1 Не пишу Не Письмо

Blocco di sincronizzazione

Un blocco sincronizzato può essere utilizzato per eseguire la sincronizzazione su qualsiasi risorsa del metodo specifica. Diciamo che in un metodo ampio (sì, sì, non puoi scrivere cose del genere, ma a volte succede) devi sincronizzare solo una piccola parte, per qualche motivo. Se inserisci tutti i codici di un metodo in un blocco sincronizzato, funzionerà come un metodo sincronizzato. La sintassi è simile alla seguente:
synchronized (“an object для блокировки”) {
   // сам code, который нужно защитить
}
Per non ripetere l'esempio precedente, creeremo thread tramite classi anonime, ovvero implementando immediatamente l'interfaccia Runnable.
/**
* Вот How добавляется блок синхронизации.
* Внутри нужно указать у кого будет взят мьютекс для блокировки.
*/
class Printer {

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

   public static void main(String args[]) {
       // один an object для двух тредов
       Printer printer = new Printer();

       // создаем два треда
       Thread writer1 = new Thread(new Runnable() {
           @Override
           public void run() {
               List<String> poem = Arrays.asList("Я ", "Writer1", " Пишу", " Письмо");
               printer.print(poem);
           }
       });
       Thread writer2 = new Thread(new Runnable() {
           @Override
           public void run() {
               List<String> poem = Arrays.asList("Не Я ", "Writer2", " Не пишу", " Не Письмо");
               printer.print(poem);
           }
       });

       // запускаем их
       writer1.start();
       writer2.start();
   }
}

}
e l'output sulla console

Я Writer1 Пишу Письмо
Не Я Writer2 Не пишу Не Письмо

Sincronizzazione statica

Se sincronizzi un metodo statico, il blocco sarà sulla classe, non sull'oggetto. In questo esempio, applichiamo la parola chiave sincronizzata a un metodo statico per eseguire la sincronizzazione statica:
/**
* Вот How добавляется блок синхронизации.
* Внутри нужно указать у кого будет взят мьютекс для блокировки.
*/
class Printer {

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

   public static void main(String args[]) {

       // создаем два треда
       Thread writer1 = new Thread(new Runnable() {
           @Override
           public void run() {
               List<String> poem = Arrays.asList("Я ", "Writer1", " Пишу", " Письмо");
               Printer.print(poem);
           }
       });
       Thread writer2 = new Thread(new Runnable() {
           @Override
           public void run() {
               List<String> poem = Arrays.asList("Не Я ", "Writer2", " Не пишу", " Не Письмо");
               Printer.print(poem);
           }
       });

       // запускаем их
       writer1.start();
       writer2.start();
   }
}
e l'output sulla console:

Не Я Writer2 Не пишу Не Письмо
Я Writer1 Пишу Письмо

47. Cos'è una variabile volatile?

La parola chiave volatileviene utilizzata nella programmazione multi-thread per garantire la sicurezza del thread poiché una modifica a una variabile modificabile è visibile a tutti gli altri thread, quindi una variabile può essere utilizzata da un thread alla volta. Usando la parola chiave, volatilepuoi garantire che la variabile sarà thread-safe e verrà archiviata nella memoria condivisa e i thread non la porteranno nella loro cache. Che cosa sembra?
private volatile AtomicInteger count;
Aggiungiamo semplicemente alla variabile volatile. Ma questo non significa completa sicurezza del thread... Dopotutto, le operazioni potrebbero non essere atomiche su una variabile. Ma puoi usare Atomicclassi che eseguono l'operazione in modo atomico, cioè in un'unica esecuzione da parte del processore. Molte di queste classi possono essere trovate nel pacchetto java.util.concurrent.atomic.

48. Cos'è lo stallo

Deadlock in Java fa parte del multithreading. Un deadlock può verificarsi in una situazione in cui un thread è in attesa di un blocco dell'oggetto acquisito da un altro thread e un secondo thread è in attesa di un blocco dell'oggetto acquisito dal primo thread. Pertanto, questi due thread si aspettano l'un l'altro e non continueranno a eseguire il proprio codice. Le 50 migliori domande e risposte per l'intervista su Java Core.  Parte 3 - 5Diamo un'occhiata a un esempio in cui è presente una classe che implementa Runnable. Accetta due risorse nel suo costruttore. All'interno del metodo run(), prende i lock uno per uno, quindi se crei due oggetti di questa classe e trasferisci le risorse in ordini diversi, puoi facilmente imbatterti in un lock:
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();
   }
}

/**
* Класс, который принимает два ресурса.
*/
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() + " захватил ресурс: " + r1);

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

           synchronized (r2) {
               System.out.println(Thread.currentThread().getName() + " захватил ресурс: " + r2);
           }
       }
   }
}
Uscita console:

Первый тред захватил первый ресурс
Второй тред захватывает второй ресурс

49. Come evitare lo stallo?

Sulla base di ciò che sappiamo su come si verifica una situazione di stallo, possiamo trarre alcune conclusioni...
  • Come mostrato nell'esempio sopra, il deadlock era dovuto all'annidamento dei lock. Cioè, all'interno di una serratura ce n'è un'altra o più. Questo può essere evitato nel modo seguente: invece di annidare, è necessario aggiungere una nuova astrazione in alto e dare il blocco a un livello superiore e rimuovere i blocchi annidati.
  • Maggiore è il blocco, maggiore è la possibilità che si verifichi una situazione di stallo. Pertanto, ogni volta che si aggiunge un lucchetto, è necessario pensare se è realmente necessario e se è possibile evitare di aggiungerne uno nuovo.
  • Usi Thread.join(). Un deadlock può verificarsi anche quando un thread ne attende un altro. Per evitare questo problema, potresti considerare di impostare un limite di tempo al join()metodo.
  • Se abbiamo un thread, non ci sarà alcun punto morto ;)

50. Cos'è una condizione di gara?

Se nelle gare reali le auto si esibiscono, allora nella terminologia racing del multi-threading, i thread si esibiscono nelle gare. Ma perché? Esistono due thread in esecuzione e che possono avere accesso allo stesso oggetto. E possono provare ad aggiornare lo stato allo stesso tempo. Finora è tutto chiaro, vero? Quindi i thread funzionano in parallelo reale (se è presente più di un core nel processore) o condizionatamente in parallelo, quando il processore assegna un breve periodo di tempo. E non possiamo controllare questi processi, quindi non possiamo garantire che quando un thread legge i dati da un oggetto, avrà il tempo di cambiarli PRIMA che lo faccia qualche altro thread. Problemi come questo si verificano quando si verifica questa combinazione test-and-act. Cosa significa? Ad esempio, abbiamo ifun'espressione nel corpo di cui cambia la condizione stessa, ovvero:
int z = 0;

// проверь
if (z < 5) {
//действуй
   z = z + 5;
}
Quindi potrebbe verificarsi una situazione in cui due thread inseriscono contemporaneamente questo blocco di codice in un momento in cui z è ancora uguale a zero e insieme modificano questo valore. E alla fine otterremo il valore atteso non 5, ma 10. Come evitarlo? È necessario bloccare prima e dopo l'esecuzione. Cioè, affinché il primo thread entri nel blocco if, esegua tutte le azioni, lo modifichi ze solo allora dia al thread successivo l'opportunità di farlo. Ma il thread successivo non entrerà nel blocco if, poiché zsarà già uguale a 5:
// получить блокировку для z
if (z < 5) {
   z = z + 5;
}
// выпустить из блокировки z
===================================================

Invece di uscita

Voglio dire grazie a tutti coloro che hanno letto fino alla fine. È stato un lungo viaggio e ce l'hai fatta! Potrebbe non essere tutto chiaro. Questo va bene. Non appena ho iniziato a imparare Java, non riuscivo a capire cosa fosse una variabile statica. Ma niente, ho dormito con questo pensiero, ho letto qualche altra fonte e finalmente ho capito. Prepararsi per un colloquio è più una questione accademica che pratica. Pertanto, prima di ogni colloquio, è necessario ripetere e rinfrescare la memoria di cose che potresti non usare molto spesso.

E come sempre link utili:

Grazie a tutti per aver letto, a presto) Il mio profilo su GitHub
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION