As 50 principais perguntas e respostas de entrevistas de emprego para Java Core. Parte 1As 50 principais perguntas e respostas de entrevistas de emprego para Java Core.  Parte 2 - 1

Multithreading

24. Como crio uma nova thread em Java?

De uma forma ou de outra, um thread é criado usando a classe Thread. Mas existem várias formas de fazer isso…
  1. Herdar java.lang.Thread .
  2. Implemente a interface java.lang.Runnable — o construtor da classe Thread usa um objeto Runnable.
Vamos falar sobre cada um deles.

Herdar a classe Thread

Nesse caso, fazemos nossa classe herdar java.lang.Thread . Ele tem um método run() , e é exatamente disso que precisamos. Toda a vida e lógica do novo thread estarão neste método. É como um método principal para o novo segmento. Depois disso, só falta criar um objeto da nossa classe e chamar o método start() . Isso criará um novo thread e começará a executar sua lógica. Vamos dar uma olhada:

/**
* 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();
   }
}
A saída do console será algo como isto:
Tópico-1 Tópico-0 Tópico-2
Ou seja, mesmo aqui vemos que as threads são executadas não na ordem, mas sim conforme a JVM achar melhor executá-las :)

Implemente a interface Runnable

Se você é contra a herança e/ou já herdou alguma outra classe, pode usar a interface java.lang.Runnable . Aqui, fazemos nossa classe implementar essa interface implementando o método run() , assim como no exemplo acima. Tudo o que resta é criar objetos Thread . Parece que mais linhas de código são piores. Mas sabemos o quão perniciosa é a herança e que é melhor evitá-la de todas as formas ;) Veja:

/**
* 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();
   }
}
E aqui está o resultado:
Tópico-0 Tópico-1 Tópico-2

25. Qual é a diferença entre um processo e um thread?

As 50 principais perguntas e respostas de entrevistas de emprego para Java Core.  Parte 2 - 2Um processo e um thread são diferentes das seguintes maneiras:
  1. Um programa em execução é chamado de processo, mas um thread é uma parte de um processo.
  2. Os processos são independentes, mas as threads são partes de um processo.
  3. Os processos têm diferentes espaços de endereçamento na memória, mas as threads compartilham um espaço de endereçamento comum.
  4. A alternância de contexto entre threads é mais rápida do que a alternância entre processos.
  5. A comunicação entre processos é mais lenta e mais cara do que a comunicação entre threads.
  6. Quaisquer alterações em um processo pai não afetam um processo filho, mas alterações em um thread pai podem afetar um thread filho.

26. Quais são os benefícios do multithreading?

  1. O multithreading permite que um aplicativo/programa sempre responda à entrada, mesmo que já esteja executando algumas tarefas em segundo plano;
  2. O multithreading torna possível concluir tarefas mais rapidamente, porque os threads são executados de forma independente;
  3. O multithreading fornece melhor uso da memória cache, porque os threads podem acessar recursos de memória compartilhada;
  4. O multithreading reduz o número de servidores necessários, porque um servidor pode executar vários threads simultaneamente.

27. Quais são os estados no ciclo de vida de um thread?

As 50 principais perguntas e respostas de entrevistas de emprego para Java Core.  Parte 2 - 3
  1. Novo: Neste estado, o objeto Thread é criado usando o operador new, mas ainda não existe um novo thread. A thread não inicia até que chamemos o método start() .
  2. Runnable: Nesse estado, o thread está pronto para ser executado após o start() método é chamado. No entanto, ele ainda não foi selecionado pelo agendador de encadeamento.
  3. Em execução: nesse estado, o agendador de encadeamentos seleciona um encadeamento de um estado pronto e o executa.
  4. Aguardando/Bloqueado: neste estado, um thread não está em execução, mas ainda está ativo ou aguardando a conclusão de outro thread.
  5. Dead/Terminated: quando um thread sai do método run() , ele está em um estado morto ou encerrado.

28. É possível executar uma thread duas vezes?

Não, não podemos reiniciar um thread, porque depois que um thread é iniciado e executado, ele entra no estado Morto. Se tentarmos iniciar um encadeamento duas vezes, uma java.lang.IllegalThreadStateException será lançada. Vamos dar uma olhada:

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();
   }
}
Haverá uma exceção assim que a execução chegar ao segundo início do mesmo thread. Tente você mesmo ;) É melhor ver isso uma vez do que ouvir sobre isso centenas de vezes.

29. E se você chamar run() diretamente sem chamar start()?

Sim, você certamente pode chamar o método run() , mas um novo thread não será criado e o método não será executado em um thread separado. Nesse caso, temos um objeto comum chamando um método comum. Se estamos falando sobre o método start() , isso é outro assunto. Quando esse método é chamado, a JVM inicia um novo encadeamento. Essa thread, por sua vez, chama nosso método ;) Não acredita? Aqui, experimente:

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 a saída do console ficará assim:
0123401234
Como você pode ver, nenhum tópico foi criado. Tudo funcionou como em uma aula normal. Primeiro, o método do primeiro objeto foi executado e depois o segundo.

30. O que é um thread daemon?

Um thread daemon é um thread que executa tarefas com prioridade mais baixa do que outro thread. Em outras palavras, sua função é realizar tarefas auxiliares que precisam ser feitas apenas em conjunto com outra thread (principal). Existem muitos encadeamentos de daemon que são executados automaticamente, como coleta de lixo, finalizador etc.

Por que o Java encerra um thread daemon?

O único propósito do encadeamento daemon é fornecer suporte em segundo plano ao encadeamento de um usuário. Da mesma forma, se o encadeamento principal for encerrado, a JVM encerrará automaticamente todos os seus encadeamentos de daemon.

Métodos da classe Thread

A classe java.lang.Thread fornece dois métodos para trabalhar com um encadeamento daemon:
  1. public void setDaemon(boolean status) — Este método indica se este será um thread daemon. O padrão é falso . Isso significa que nenhum encadeamento de daemon será criado, a menos que você diga especificamente.
  2. public boolean isDaemon() — Este método é essencialmente um getter para a variável daemon , que definimos usando o método anterior.
Exemplo:

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();
   }
}
Saída do console:
demônio? demônio verdadeiro? demônio falso? thread daemon falso thread do usuário thread do usuário
A partir da saída, vemos que dentro do próprio thread, podemos usar o método estático currentThread() para descobrir qual thread é. Alternativamente, se tivermos uma referência ao objeto thread, também podemos descobrir diretamente a partir dele. Isso fornece o nível necessário de configurabilidade.

31. É possível transformar uma thread em daemon depois de criada?

Não. Se você tentar fazer isso, receberá uma IllegalThreadStateException . Isso significa que só podemos criar um thread daemon antes de iniciar. Exemplo:

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);
   }
}
Saída do console:
Trabalhando... Exceção no encadeamento "principal" java.lang.IllegalThreadStateException em java.lang.Thread.setDaemon(Thread.java:1359) em SetDaemonAfterStartExample.main(SetDaemonAfterStartExample.java:14)

32. O que é um gancho de desligamento?

Um gancho de encerramento é um encadeamento que é chamado implicitamente antes que a Java virtual machine (JVM) seja encerrada. Assim, podemos usá-lo para liberar um recurso ou salvar o estado quando a máquina virtual Java desligar normalmente ou anormalmente. Podemos adicionar um gancho de desligamento usando o seguinte método:

Runtime.getRuntime().addShutdownHook(new ShutdownHookThreadExample());
Como mostrado no exemplo:

/**
* 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();
       }
   }
}
Saída do console:
Agora o programa vai adormecer. Pressione Ctrl+C para finalizá-lo. gancho de desligamento executado

33. O que é sincronização?

Em Java, a sincronização é a capacidade de controlar o acesso de vários threads a qualquer recurso compartilhado. Quando vários threads tentam executar a mesma tarefa simultaneamente, você pode obter um resultado incorreto. Para corrigir esse problema, o Java usa a sincronização, que permite que apenas um encadeamento seja executado por vez. A sincronização pode ser obtida de três maneiras:
  • Sincronizando um método
  • Sincronizando um bloco específico
  • Sincronização estática

Sincronizando um método

Um método sincronizado é usado para bloquear um objeto para qualquer recurso compartilhado. Quando um thread chama um método sincronizado, ele adquire automaticamente o bloqueio do objeto e o libera quando o thread conclui sua tarefa. Para fazer isso funcionar, você precisa adicionar a palavra-chave sincronizada . Podemos ver como isso funciona observando um exemplo:

/**
* 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 a saída do console é esta:
I Thread-0 Escrevo Uma Carta I Not Thread-1 Não Escrevo Nenhuma Carta

bloco de sincronização

Um bloco sincronizado pode ser usado para executar a sincronização em qualquer recurso específico em um método. Digamos que em um método grande (sim, você não deveria escrevê-los, mas às vezes eles acontecem) você precisa sincronizar apenas uma pequena seção por algum motivo. Se você colocar todo o código do método em um bloco sincronizado, ele funcionará da mesma forma que um método sincronizado. A sintaxe fica assim:

synchronized ("object to be locked") {
   // The code that must be protected
}
Para não repetir o exemplo anterior, criaremos threads usando classes anônimas, ou seja, implementaremos imediatamente a interface 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 a saída do console é esta:
Eu Escritor1 Escrevo Uma Carta Eu Não Escritor2 Não Escrevo Nenhuma Carta

Sincronização estática

Se você fizer um método estático sincronizado, o bloqueio acontecerá na classe, não no objeto. Neste exemplo, realizamos a sincronização estática aplicando a palavra-chave sincronizar a um método estático:

/**
* 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 a saída do console é esta:
Eu Não Escritor2 Não Escrevo Nenhuma Carta Eu Escritor1 Escrevo Uma Carta

34. O que é uma variável volátil?

Na programação multithread, a palavra-chave volátil é usada para segurança de thread. Quando uma variável mutável é modificada, a alteração fica visível para todos os outros threads, portanto, uma variável pode ser usada por um thread por vez. Ao usar a palavra-chave volátil , você pode garantir que uma variável seja thread-safe e armazenada na memória compartilhada e que os threads não a armazenem em seus caches. O que isso parece?

private volatile AtomicInteger count;
Nós apenas adicionamos volátil à variável. Mas lembre-se de que isso não significa segurança total do thread... Afinal, as operações na variável podem não ser atômicas. Dito isso, você pode usar classes Atomic que realizam operações atomicamente, ou seja, em uma única instrução da CPU. Existem muitas dessas classes no pacote java.util.concurrent.atomic .

35. O que é impasse?

Em Java, deadlock é algo que pode acontecer como parte do multithreading. Um deadlock pode ocorrer quando um thread está esperando pelo bloqueio de um objeto adquirido por outro thread, e o segundo thread está esperando pelo bloqueio do objeto adquirido pelo primeiro thread. Isso significa que os dois threads estão esperando um pelo outro e a execução de seu código não pode continuar. As 50 principais perguntas e respostas de entrevistas de emprego para Java Core.  Parte 2 - 4Vamos considerar um exemplo que possui uma classe que implementa Runnable. Seu construtor requer dois recursos. O método run() adquire o bloqueio para eles em ordem. Se você criar dois objetos desta classe e passar os recursos em uma ordem diferente, poderá facilmente entrar em um impasse:

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);
           }
       }
   }
}
Saída do console:
A primeira thread adquiriu o primeiro recurso A segunda thread adquiriu o segundo recurso

36. Como você evita o impasse?

Como sabemos como ocorre o impasse, podemos tirar algumas conclusões...
  • No exemplo acima, o deadlock ocorre devido ao fato de termos bloqueio aninhado. Ou seja, temos um bloco sincronizado dentro de um bloco sincronizado. Para evitar isso, em vez de aninhar, você precisa criar uma nova camada de abstração superior, mover a sincronização para o nível superior e eliminar o bloqueio aninhado.
  • Quanto mais bloqueios você fizer, maior a probabilidade de haver um impasse. Portanto, cada vez que adicionar um bloco sincronizado, você precisa pensar se realmente precisa dele e se pode evitar adicionar um novo.
  • Usando Thread.join() . Você também pode entrar em um impasse enquanto um thread espera por outro. Para evitar esse problema, considere definir um tempo limite para o método join() .
  • Se tivermos um thread, não haverá impasse;)

37. O que é uma condição de corrida?

Se as corridas da vida real envolvem carros, as corridas em multithreading envolvem threads. Mas por que? :/ Existem dois threads em execução e podem acessar o mesmo objeto. E eles podem tentar atualizar o estado do objeto compartilhado ao mesmo tempo. Tudo está claro até agora, certo? Os threads são executados literalmente em paralelo (se o processador tiver mais de um núcleo) ou sequencialmente, com o processador alocando fatias de tempo intercaladas. Não podemos gerenciar esses processos. Isso significa que, quando um thread lê dados de um objeto, não podemos garantir que ele terá tempo para alterar o objeto ANTES que outro thread o faça. Esses problemas surgem quando temos esses combos de "check-and-act". O que isso significa? Suponha que temos uma instrução if cujo corpo altera a própria condição if, por exemplo:

int z = 0;

// Check
if (z < 5) {
// Act
   z = z + 5;
}
Dois threads podem entrar simultaneamente neste bloco de código quando z ainda é zero e, em seguida, ambos os threads podem alterar seu valor. Como resultado, não obteremos o valor esperado de 5. Em vez disso, obteríamos 10. Como evitar isso? Você precisa adquirir um bloqueio antes de verificar e agir e, em seguida, liberar o bloqueio depois. Ou seja, você precisa fazer com que o primeiro thread entre no bloco if , execute todas as ações, altere z e só então dê ao próximo thread a oportunidade de fazer o mesmo. Mas a próxima thread não entrará no bloco if , já que z agora será 5:

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

Em vez de uma conclusão

Quero agradecer a todos que leram até o final. Foi um longo caminho, mas você aguentou! Talvez nem tudo esteja claro. Isto é normal. Quando comecei a estudar Java, não conseguia entender o que é uma variável estática. Mas não é grande coisa. Eu dormi sobre isso, li mais algumas fontes e então o entendimento veio. Preparar-se para uma entrevista é mais uma questão acadêmica do que prática. Como resultado, antes de cada entrevista, você deve revisar e refrescar em sua memória as coisas que você não usa com muita frequência.

E como sempre, aqui estão alguns links úteis:

Obrigado a todos por ler. Até breve :) Meu perfil do GitHubAs 50 principais perguntas e respostas de entrevistas de emprego para Java Core.  Parte 2 - 5