JavaRush /Blog Java /Random-ES /Multihilo en Java: esencia, ventajas y errores comunes

Multihilo en Java: esencia, ventajas y errores comunes

Publicado en el grupo Random-ES
¡Hola! Primero que nada, felicidades: ¡has llegado al tema de Multithreading en Java! Este es un logro importante, queda un largo camino por recorrer. Pero prepárate: este es uno de los temas más difíciles del curso. Y la cuestión no es que aquí se utilicen clases complejas o muchos métodos: al contrario, no hay ni dos docenas. Es más, necesitas cambiar un poco tu forma de pensar. Anteriormente, sus programas se ejecutaban secuencialmente. Algunas líneas de código siguieron a otras, algunos métodos siguieron a otros y, en general, todo quedó claro. Primero, calcule algo, luego muestre el resultado en la consola y luego finalice el programa. Para comprender el subproceso múltiple, es mejor pensar en términos de concurrencia. Empecemos por algo muy sencillo :) Multihilo en Java: esencia, ventajas y errores comunes - 1Imagina que tu familia se muda de una casa a otra. Una parte importante de la mudanza es empacar sus libros. Has acumulado muchos libros y necesitas ponerlos en cajas. Ahora sólo tú eres libre. Mamá está preparando la comida, el hermano está recogiendo ropa y la hermana ha ido a la tienda. Al menos podrás arreglártelas solo y, tarde o temprano, incluso podrás completar la tarea tú mismo, pero te llevará mucho tiempo. Sin embargo, en 20 minutos tu hermana regresará de la tienda y no tiene nada más que hacer. Para que ella pueda unirse a ti. La tarea seguía siendo la misma: meter los libros en cajas. Simplemente corre el doble de rápido. ¿Por qué? Porque el trabajo se hace en paralelo. Dos “hilos” diferentes (tú y tu hermana) están realizando simultáneamente la misma tarea y, si nada cambia, la diferencia de tiempo será muy grande en comparación con una situación en la que harías todo solo. Si tu hermano completa su tarea pronto, podrá ayudarte y todo irá aún más rápido.

Problemas que resuelve el multithreading en Java

Básicamente, el subproceso múltiple de Java se inventó para resolver dos problemas principales:
  1. Realiza múltiples acciones al mismo tiempo.

    En el ejemplo anterior, diferentes hilos (es decir, miembros de la familia) realizaron varias acciones en paralelo: lavaron los platos, fueron a la tienda, doblaron cosas.

    Se puede dar un ejemplo más de "programador". Imagine que tiene un programa con una interfaz de usuario. Cuando se hace clic en el botón Continuar, deberían realizarse algunos cálculos dentro del programa y el usuario debería ver la siguiente pantalla de interfaz. Si estas acciones se llevan a cabo secuencialmente, después de hacer clic en el botón "Continuar", el programa simplemente se congelará. El usuario verá la misma pantalla con un botón “Continuar” hasta que se completen todos los cálculos internos y el programa llegue a la parte donde se comenzará a dibujar la interfaz.

    Bueno, ¡esperemos un par de minutos!

    Multihilo en Java: esencia, ventajas y errores comunes - 3

    También podemos rehacer nuestro programa o, como dicen los programadores, "paralelizarlo". Deje que los cálculos necesarios se realicen en un hilo y la representación de la interfaz en otro. La mayoría de las computadoras tienen suficientes recursos para esto. En este caso, el programa no será "estúpido" y el usuario se moverá tranquilamente entre las pantallas de la interfaz, sin preocuparse por lo que sucede en el interior. No interfiere :)

  2. Acelera los cálculos.

    Aquí todo es mucho más sencillo. Si nuestro procesador tiene varios núcleos, y la mayoría de procesadores ahora son multinúcleo, nuestra lista de tareas se puede resolver en paralelo con varios núcleos. Obviamente, si necesitamos resolver 1000 problemas y cada uno de ellos se resuelve en un segundo, un núcleo resolverá la lista en 1000 segundos, dos núcleos en 500 segundos, tres en poco más de 333 segundos, y así sucesivamente.

Pero, como ya leyó en la conferencia, los sistemas modernos son muy inteligentes e incluso en un núcleo informático pueden implementar el paralelismo o pseudoparalelismo cuando las tareas se realizan alternativamente. Pasemos de lo general a lo específico y familiaricémonos con la clase principal de la biblioteca Java relacionada con subprocesos múltiples: java.lang.Thread. Estrictamente hablando, los subprocesos en Java están representados por instancias de la clase Thread. Es decir, para crear y ejecutar 10 subprocesos, necesitará 10 objetos de esta clase. Escribamos el ejemplo más simple:
public class MyFirstThread extends Thread {

   @Override
   public void run() {
       System.out.println("I'm Thread! My name is " + getName());
   }
}
Para crear y lanzar subprocesos, necesitamos crear una clase y heredarla del archivo java.lang. Thready anular el método en él run(). El último es muy importante. Es en el método donde run()prescribimos la lógica que nuestro hilo debe ejecutar. Ahora, si creamos una instancia MyFirstThready la ejecutamos, el método run()imprimirá una línea con su nombre en la consola: el método getName()imprime el nombre “sistema” del hilo, que se asigna automáticamente. Aunque, en realidad, ¿por qué “si”? ¡Creemos y probemos!
public class Main {

   public static void main(String[] args) {

       for (int i = 0; i < 10; i++) {

           MyFirstThread thread = new MyFirstThread();
           thread.start();
       }
   }
}
Salida de la consola: ¡Soy Thread! Mi nombre es Thread-2 ¡Soy Thread! Mi nombre es Thread-1 ¡Soy Thread! Mi nombre es Thread-0 ¡Soy Thread! Mi nombre es Thread-3 ¡Soy Thread! Mi nombre es Thread-6 ¡Soy Thread! Mi nombre es Thread-7 ¡Soy Thread! Mi nombre es Thread-4 ¡Soy Thread! Mi nombre es Thread-5 ¡Soy Thread! Mi nombre es Thread-9 ¡Soy Thread! Mi nombre es Thread-8 Creamos 10 subprocesos (objetos) MyFirstThreadque heredan Thready los lanzan llamando al método del objeto start(). Después de llamar a un método , start()su método comienza a funcionar run()y se ejecuta la lógica escrita en él. Tenga en cuenta: los nombres de los hilos no están en orden. Es bastante extraño, ¿por qué no fueron ejecutados por turnos: Thread-0, Thread-1, Thread-2etc.? Este es exactamente un ejemplo de cuando el pensamiento estándar y “secuencial” no funciona. El hecho es que en este caso solo emitimos comandos para crear y ejecutar 10 hilos. El orden en que deben iniciarse lo decide el programador de subprocesos: un mecanismo especial dentro del sistema operativo. Cómo está exactamente estructurado y según qué principio toma decisiones es un tema muy complejo y no profundizaremos en él ahora. Lo principal a recordar es que el programador no puede controlar la secuencia de ejecución del hilo. Para darse cuenta de la gravedad de la situación, intente ejecutar el método main()del ejemplo anterior un par de veces más. Segunda salida de la consola: ¡ Soy Thread! Mi nombre es Thread-0 ¡Soy Thread! Mi nombre es Thread-4 ¡Soy Thread! Mi nombre es Thread-3 ¡Soy Thread! Mi nombre es Thread-2 ¡Soy Thread! Mi nombre es Thread-1 ¡Soy Thread! Mi nombre es Thread-5 ¡Soy Thread! Mi nombre es Thread-6 ¡Soy Thread! Mi nombre es Thread-8 ¡Soy Thread! Mi nombre es Thread-9 ¡Soy Thread! Mi nombre es Thread-7 Tercera salida de consola: ¡ Soy Thread! Mi nombre es Thread-0 ¡Soy Thread! Mi nombre es Thread-3 ¡Soy Thread! Mi nombre es Thread-1 ¡Soy Thread! Mi nombre es Thread-2 ¡Soy Thread! Mi nombre es Thread-6 ¡Soy Thread! Mi nombre es Thread-4 ¡Soy Thread! Mi nombre es Thread-9 ¡Soy Thread! Mi nombre es Thread-5 ¡Soy Thread! Mi nombre es Thread-7 ¡Soy Thread! Mi nombre es Hilo-8

Problemas que crea el subproceso múltiple

En el ejemplo de los libros, viste que el subproceso múltiple resuelve problemas bastante importantes y su uso acelera el trabajo de nuestros programas. En muchos casos, muchas veces. Pero no en vano el multithreading se considera un tema complejo. Después de todo, si se usa incorrectamente, crea problemas en lugar de resolverlos. Cuando digo "crear problemas", no me refiero a algo abstracto. Hay dos problemas específicos que pueden causar los subprocesos múltiples: interbloqueo y condición de carrera. Un punto muerto es una situación en la que varios subprocesos esperan recursos ocupados entre sí y ninguno de ellos puede continuar ejecutándose. Hablaremos más sobre esto en futuras conferencias, pero por ahora este ejemplo será suficiente: Multihilo en Java: esencia, ventajas y errores comunes - 4 imagine que el subproceso 1 está trabajando con algún Objeto-1 y el subproceso 2 está trabajando con el Objeto-2. El programa está escrito así:
  1. Thread-1 dejará de funcionar con el Objeto-1 y cambiará al Objeto-2 tan pronto como Thread-2 deje de funcionar con el Objeto 2 y cambie al Objeto-1.
  2. Thread-2 dejará de funcionar con el Objeto-2 y cambiará al Objeto-1 tan pronto como el Hilo-1 deje de funcionar con el Objeto 1 y cambie al Objeto-2.
Incluso sin un conocimiento profundo de subprocesos múltiples, puede comprender fácilmente que no saldrá nada de ello. Los hilos nunca cambiarán de lugar y se esperarán para siempre. El error parece obvio, pero en realidad no lo es. Puedes permitirlo fácilmente en el programa. Veremos ejemplos de código que causa un punto muerto en las siguientes conferencias. Por cierto, Quora tiene un excelente ejemplo de la vida real que explica qué es un punto muerto . “En algunos estados de la India no te venden tierras agrícolas a menos que estés registrado como agricultor. Sin embargo, no quedará registrado como agricultor si no posee tierras agrícolas”. Genial, ¡qué puedo decir! :) Ahora sobre las condiciones de la carrera: el estado de la carrera. Multihilo en Java: esencia, ventajas y errores comunes - 5Una condición de carrera es un defecto de diseño en un sistema o aplicación multiproceso en el que el funcionamiento del sistema o aplicación depende del orden en que se ejecutan partes del código. Recuerde el ejemplo con subprocesos en ejecución:
public class MyFirstThread extends Thread {

   @Override
   public void run() {
       System.out.println("Выполнен поток " + getName());
   }
}

public class Main {

   public static void main(String[] args) {

       for (int i = 0; i < 10; i++) {

           MyFirstThread thread = new MyFirstThread();
           thread.start();
       }
   }
}
¡Ahora imagina que el programa es responsable del funcionamiento de un robot que prepara comida! Thread-0 saca los huevos del frigorífico. El flujo 1 enciende la estufa. Stream-2 saca una sartén y la pone al fuego. El arroyo 3 enciende un fuego en la estufa. El chorro 4 vierte aceite en la sartén. El chorro 5 rompe los huevos y los vierte en la sartén. El arroyo 6 tira las conchas a la basura. Stream-7 retira los huevos revueltos terminados del fuego. Potok-8 pone huevos revueltos en un plato. El arroyo 9 lava los platos. Mire los resultados de nuestro programa: Hilo-0 ejecutado Hilo-2 hilo ejecutado Hilo-1 hilo ejecutado Hilo-4 hilo ejecutado Hilo-9 hilo ejecutado Hilo-5 hilo ejecutado Hilo-8 hilo ejecutado Hilo-7 hilo ejecutado Hilo-7 hilo ejecutado -3 hilo-6 hilo ejecutado ¿ Es divertido el script? :) Y todo porque el funcionamiento de nuestro programa depende del orden en que se ejecutan los hilos. A la más mínima violación de la secuencia, nuestra cocina se convierte en un infierno y un robot enloquecido destruye todo lo que la rodea. Este también es un problema común en la programación multiproceso, del que escuchará más de una vez. Al final de la conferencia, me gustaría recomendarles un libro sobre subprocesos múltiples.
Multihilo en Java: esencia, ventajas y errores comunes - 6
"Java Concurrency in Practice" se escribió en 2006, pero no ha perdido su relevancia. Cubre la programación multiproceso en Java, comenzando desde lo básico y terminando con una lista de los errores y antipatrones más comunes. Si alguna vez decides convertirte en un gurú de la programación multiproceso, debes leer este libro. ¡Nos vemos en las próximas conferencias! :)
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION