CodeGym/Blog Java/Random-ES/Las 50 mejores preguntas y respuestas de entrevistas de t...
John Squirrels
Nivel 41
San Francisco

Las 50 mejores preguntas y respuestas de entrevistas de trabajo para Java Core. Parte 2

Publicado en el grupo Random-ES
Las 50 mejores preguntas y respuestas de entrevistas de trabajo para Java Core. Parte 1Las 50 mejores preguntas y respuestas de entrevistas de trabajo para Java Core.  parte 2 - 1

subprocesos múltiples

24. ¿Cómo creo un hilo nuevo en Java?

De una forma u otra, un hilo se crea usando la clase Thread. Pero hay varias maneras de hacer esto…
  1. Heredar java.lang.Thread .
  2. Implemente la interfaz java.lang.Runnable : el constructor de la clase Thread toma un objeto Runnable.
Hablemos de cada uno de ellos.

Heredar la clase Thread

En este caso, hacemos que nuestra clase herede java.lang.Thread . Tiene un método run() , y eso es justo lo que necesitamos. Toda la vida y lógica del nuevo hilo estará en este método. Es como un método principal para el nuevo hilo. Después de eso, todo lo que queda es crear un objeto de nuestra clase y llamar al método start() . Esto creará un nuevo hilo y comenzará a ejecutar su lógica. Vamos a ver:
/**
* 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();
   }
}
La salida de la consola será algo como esto:
Subproceso-1 Subproceso-0 Subproceso-2
Es decir, incluso aquí vemos que los subprocesos no se ejecutan en orden, sino como la JVM considera adecuado ejecutarlos :)

Implementar la interfaz Runnable

Si está en contra de la herencia y/o ya heredó alguna otra clase, puede usar la interfaz java.lang.Runnable . Aquí, hacemos que nuestra clase implemente esta interfaz implementando el método run() , tal como en el ejemplo anterior. Todo lo que queda es crear objetos Thread . Parecería que más líneas de código son peores. Pero sabemos lo perniciosa que es la herencia y que es mejor evitarla por todos los medios ;) Echa un vistazo:
/**
* 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();
   }
}
Y aquí está el resultado:
Subproceso-0 Subproceso-1 Subproceso-2

25. ¿Cuál es la diferencia entre un proceso y un hilo?

Las 50 mejores preguntas y respuestas de entrevistas de trabajo para Java Core.  Parte 2 - 2Un proceso y un subproceso son diferentes de las siguientes maneras:
  1. Un programa en ejecución se denomina proceso, pero un subproceso es una parte de un proceso.
  2. Los procesos son independientes, pero los hilos son partes de un proceso.
  3. Los procesos tienen diferentes espacios de direcciones en la memoria, pero los subprocesos comparten un espacio de direcciones común.
  4. El cambio de contexto entre subprocesos es más rápido que el cambio entre procesos.
  5. La comunicación entre procesos es más lenta y costosa que la comunicación entre subprocesos.
  6. Cualquier cambio en un proceso principal no afecta a un proceso secundario, pero los cambios en un subproceso principal pueden afectar a un subproceso secundario.

26. ¿Cuáles son los beneficios de los subprocesos múltiples?

  1. Multithreading permite que una aplicación/programa siempre responda a la entrada, incluso si ya está ejecutando algunas tareas en segundo plano;
  2. Los subprocesos múltiples permiten completar tareas más rápido, porque los subprocesos se ejecutan de forma independiente;
  3. Los subprocesos múltiples proporcionan un mejor uso de la memoria caché, ya que los subprocesos pueden acceder a los recursos de memoria compartidos;
  4. Los subprocesos múltiples reducen la cantidad de servidores necesarios, ya que un servidor puede ejecutar varios subprocesos simultáneamente.

27. ¿Cuáles son los estados en el ciclo de vida de un hilo?

Las 50 mejores preguntas y respuestas de entrevistas de trabajo para Java Core.  Parte 2 - 3
  1. Nuevo: en este estado, el objeto Subproceso se crea con el operador nuevo, pero aún no existe un nuevo subproceso. El hilo no comienza hasta que llamamos al método start() .
  2. Ejecutable: en este estado, el subproceso está listo para ejecutarse después del inicio () se llama el método. Sin embargo, aún no ha sido seleccionado por el programador de subprocesos.
  3. En ejecución: en este estado, el programador de subprocesos elige un subproceso de un estado listo y se ejecuta.
  4. Esperando/Bloqueado: en este estado, un subproceso no se está ejecutando, pero aún está vivo o esperando que se complete otro subproceso.
  5. Muerto/Terminado: cuando un subproceso sale del método run() , está en un estado muerto o terminado.

28. ¿Es posible ejecutar un hilo dos veces?

No, no podemos reiniciar un subproceso, porque después de que un subproceso se inicia y se ejecuta, pasa al estado Muerto. Si intentamos iniciar un hilo dos veces, se lanzará una java.lang.IllegalThreadStateException . Vamos a ver:
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();
   }
}
Habrá una excepción tan pronto como la ejecución llegue al segundo inicio del mismo hilo. Pruébelo usted mismo ;) Es mejor ver esto una vez que escucharlo cien veces.

29. ¿Qué sucede si llama a run() directamente sin llamar a start()?

Sí, ciertamente puede llamar al método run() , pero no se creará un nuevo hilo y el método no se ejecutará en un hilo separado. En este caso, tenemos un objeto ordinario llamando a un método ordinario. Si estamos hablando del método start() , entonces ese es otro asunto. Cuando se llama a este método, la JVM inicia un nuevo subproceso. Este hilo, a su vez, llama a nuestro método ;) ¿No te lo crees? Aquí, pruébalo:
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();
   }
}
Y la salida de la consola se verá así:
0123401234
Como puede ver, no se creó ningún hilo. Todo funcionó como en una clase ordinaria. Primero se ejecutó el método del primer objeto y luego el segundo.

30. ¿Qué es un subproceso daemon?

Un subproceso daemon es un subproceso que realiza tareas con una prioridad más baja que otro subproceso. En otras palabras, su trabajo es realizar tareas auxiliares que deben realizarse solo junto con otro subproceso (principal). Hay muchos subprocesos de daemon que se ejecutan automáticamente, como la recolección de basura, el finalizador, etc.

¿Por qué Java termina un subproceso de daemon?

El único propósito del subproceso daemon es proporcionar soporte en segundo plano al subproceso de un usuario. En consecuencia, si el subproceso principal finaliza, la JVM finaliza automáticamente todos sus subprocesos daemon.

Métodos de la clase Thread

La clase java.lang.Thread proporciona dos métodos para trabajar con un subproceso daemon:
  1. public void setDaemon(boolean status) — Este método indica si se tratará de un subproceso daemon. El valor predeterminado es falso . Esto significa que no se crearán subprocesos de daemon a menos que usted lo indique específicamente.
  2. public boolean isDaemon() — Este método es esencialmente un getter para la variable daemon , que configuramos usando el método anterior.
Ejemplo:
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();
   }
}
Salida de la consola:
¿demonio? verdadero demonio? falso demonio? subproceso de daemon falso subproceso de usuario subproceso de usuario
A partir de la salida, vemos que dentro del propio subproceso, podemos usar el método estático currentThread() para averiguar qué subproceso es. Alternativamente, si tenemos una referencia al objeto hilo, también podemos averiguarlo directamente a partir de él. Esto proporciona el nivel necesario de configurabilidad.

31. ¿Es posible convertir un hilo en un demonio después de haberlo creado?

No. Si intenta hacer esto, obtendrá una excepción IllegalThreadStateException . Esto significa que solo podemos crear un subproceso de daemon antes de que comience. Ejemplo:
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);
   }
}
Salida de la consola:
Trabajando... Excepción en el hilo "principal" java.lang.IllegalThreadStateException en java.lang.Thread.setDaemon(Thread.java:1359) en SetDaemonAfterStartExample.main(SetDaemonAfterStartExample.java:14)

32. ¿Qué es un gancho de apagado?

Un enlace de apagado es un subproceso al que se llama implícitamente antes de que se apague la máquina virtual Java (JVM). Por lo tanto, podemos usarlo para liberar un recurso o guardar un estado cuando la máquina virtual Java se apaga de manera normal o anormal. Podemos agregar un enlace de apagado usando el siguiente método:
Runtime.getRuntime().addShutdownHook(new ShutdownHookThreadExample());
Como se muestra en el ejemplo:
/**
* 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();
       }
   }
}
Salida de la consola:
Ahora el programa se va a quedar dormido. Presione Ctrl+C para terminarlo. gancho de apagado ejecutado

33. ¿Qué es la sincronización?

En Java, la sincronización es la capacidad de controlar el acceso de varios subprocesos a cualquier recurso compartido. Cuando varios subprocesos intentan realizar la misma tarea simultáneamente, podría obtener un resultado incorrecto. Para solucionar este problema, Java utiliza la sincronización, que permite que solo se ejecute un subproceso a la vez. La sincronización se puede lograr de tres maneras:
  • Sincronizar un método
  • Sincronizar un bloque específico
  • sincronización estática

Sincronizar un método

Se utiliza un método sincronizado para bloquear un objeto para cualquier recurso compartido. Cuando un subproceso llama a un método sincronizado, automáticamente adquiere el bloqueo del objeto y lo libera cuando el subproceso completa su tarea. Para que esto funcione, debe agregar la palabra clave sincronizada . Podemos ver cómo funciona esto mirando un ejemplo:
/**
* 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);
   }
}
Y la salida de la consola es esta:
I Thread-0 Write A Letter I No Thread-1 No escribir ninguna carta

Bloque de sincronización

Un bloque sincronizado se puede usar para realizar la sincronización en cualquier recurso en particular en un método. Digamos que en un método grande (sí, no debería escribirlos, pero a veces suceden) necesita sincronizar solo una pequeña sección por alguna razón. Si coloca todo el código del método en un bloque sincronizado, funcionará igual que un método sincronizado. La sintaxis se ve así:
synchronized ("object to be locked") {
   // The code that must be protected
}
Para evitar repetir el ejemplo anterior, crearemos hilos utilizando clases anónimas, es decir, implementaremos inmediatamente la interfaz 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();
   }
}

}
Y la salida de la consola es esta:
Yo escritor1 escribir una carta yo no escritor2 no escribir ninguna carta

sincronización estática

Si sincroniza un método estático, el bloqueo ocurrirá en la clase, no en el objeto. En este ejemplo, realizamos una sincronización estática aplicando la palabra clave sincronizada a un 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();
   }
}
Y la salida de la consola es esta:
No Escribo2 No Escribo Ninguna Carta Escribo1 Escribo Una Carta

34. ¿Qué es una variable volátil?

En la programación de subprocesos múltiples, la palabra clave volatile se usa para seguridad de subprocesos. Cuando se modifica una variable mutable, el cambio es visible para todos los demás subprocesos, por lo que una variable puede ser utilizada por un subproceso a la vez. Al usar la palabra clave volatile , puede garantizar que una variable sea segura para subprocesos y se almacene en la memoria compartida, y que los subprocesos no la almacenarán en sus cachés. A qué se parece esto?
private volatile AtomicInteger count;
Simplemente agregamos volátil a la variable. Pero tenga en cuenta que esto no significa seguridad completa para subprocesos... Después de todo, las operaciones en la variable pueden no ser atómicas. Dicho esto, puede utilizar clases atómicas que realizan operaciones de forma atómica, es decir, en una única instrucción de CPU. Hay muchas clases de este tipo en el paquete java.util.concurrent.atomic .

35. ¿Qué es un punto muerto?

En Java, el interbloqueo es algo que puede ocurrir como parte de los subprocesos múltiples. Un interbloqueo puede ocurrir cuando un subproceso está esperando el bloqueo de un objeto adquirido por otro subproceso, y el segundo subproceso está esperando el bloqueo del objeto adquirido por el primer subproceso. Esto significa que los dos subprocesos se esperan el uno al otro y la ejecución de su código no puede continuar. Las 50 mejores preguntas y respuestas de entrevistas de trabajo para Java Core.  Parte 2 - 4Consideremos un ejemplo que tiene una clase que implementa Runnable. Su constructor toma dos recursos. El método run() adquiere el bloqueo para ellos en orden. Si crea dos objetos de esta clase y pasa los recursos en un orden diferente, puede encontrarse fácilmente en un punto muerto:
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);
           }
       }
   }
}
Salida de la consola:
El primer subproceso adquirió el primer recurso El segundo subproceso adquirió el segundo recurso

36. ¿Cómo se evita el punto muerto?

Como sabemos cómo se produce el interbloqueo, podemos sacar algunas conclusiones...
  • En el ejemplo anterior, el interbloqueo ocurre debido al hecho de que hemos anidado el bloqueo. Es decir, tenemos un bloque sincronizado dentro de un bloque sincronizado. Para evitar esto, en lugar de anidar, debe crear una nueva capa de abstracción superior, mover la sincronización al nivel superior y eliminar el bloqueo anidado.
  • Cuanto más bloqueo haga, más probable es que haya un interbloqueo. Por lo tanto, cada vez que agregue un bloque sincronizado, debe pensar si realmente lo necesita y si puede evitar agregar uno nuevo.
  • Usando Thread.join() . También puede encontrarse en un punto muerto mientras un subproceso espera a otro. Para evitar este problema, podría considerar establecer un tiempo de espera para el método join() .
  • Si tenemos un hilo, entonces no habrá interbloqueo;)

37. ¿Qué es una condición de carrera?

Si las carreras de la vida real involucran autos, entonces las carreras en subprocesos múltiples involucran subprocesos. ¿Pero por qué? :/ Hay dos subprocesos que se están ejecutando y pueden acceder al mismo objeto. Y pueden intentar actualizar el estado del objeto compartido al mismo tiempo. Todo está claro hasta ahora, ¿verdad? Los subprocesos se ejecutan literalmente en paralelo (si el procesador tiene más de un núcleo) o secuencialmente, con el procesador asignando intervalos de tiempo intercalados. No podemos gestionar estos procesos. Esto significa que cuando un subproceso lee datos de un objeto, no podemos garantizar que tendrá tiempo de cambiar el objeto ANTES de que otro subproceso lo haga. Tales problemas surgen cuando tenemos estos combos de "verificar y actuar". ¿Qué significa eso? Supongamos que tenemos una declaración if cuyo cuerpo cambia la condición if en sí misma, por ejemplo:
int z = 0;

// Check
if (z < 5) {
// Act
   z = z + 5;
}
Dos subprocesos podrían ingresar simultáneamente a este bloque de código cuando z todavía es cero y luego ambos subprocesos podrían cambiar su valor. Como resultado, no obtendremos el valor esperado de 5. En su lugar, obtendremos 10. ¿Cómo se evita esto? Debe adquirir un bloqueo antes de verificar y actuar, y luego liberar el bloqueo después. Es decir, debe hacer que el primer subproceso ingrese el bloque if , realice todas las acciones, cambie z y solo luego le dé al siguiente subproceso la oportunidad de hacer lo mismo. Pero el siguiente hilo no entrará en el bloque if , ya que z ahora será 5:
// Acquire the lock for z
if (z < 5) {
   z = z + 5;
}
// Release z's lock
===================================================

En lugar de una conclusión

Quiero dar las gracias a todos los que leyeron hasta el final. Fue un largo camino, pero aguantaste! Quizá no todo esté claro. Esto es normal. Cuando comencé a estudiar Java, no podía entender qué es una variable estática. Pero no es gran cosa. Me quedé dormido, leí algunas fuentes más y luego llegó el entendimiento. Prepararse para una entrevista es más una cuestión académica que práctica. Como resultado, antes de cada entrevista, debe revisar y refrescar en su memoria aquellas cosas que quizás no use con mucha frecuencia.

Y como siempre, aquí hay algunos enlaces útiles:

Gracias a todos por leer. Hasta pronto :) Mi perfil de GitHubLas 50 mejores preguntas y respuestas de entrevistas de trabajo para Java Core.  Parte 2 - 5
Comentarios
  • Populares
  • Nuevas
  • Antiguas
Debes iniciar sesión para dejar un comentario
Esta página aún no tiene comentarios