Comportamiento del subproceso en la JVM

El subproceso se refiere a la práctica de ejecutar procesos de programación al mismo tiempo para mejorar el rendimiento de la aplicación. Si bien no es tan común trabajar con subprocesos directamente en aplicaciones comerciales, se usan todo el tiempo en los marcos de Java.

Por ejemplo, los marcos que procesan un gran volumen de información, como Spring Batch, usan subprocesos para administrar datos. La manipulación de subprocesos o procesos de CPU simultáneamente mejora el rendimiento, lo que resulta en programas más rápidos y eficientes.

Obtén el código fuente

Obtenga el código para este Java Challenger. Puede ejecutar sus propias pruebas mientras sigue los ejemplos.

Encuentra tu primer hilo: método main () de Java

Incluso si nunca ha trabajado directamente con subprocesos de Java, ha trabajado indirectamente con ellos porque el método main () de Java contiene un subproceso principal. Siempre que ha ejecutado el main()método, también ha ejecutado main Thread.

Estudiar la Threadclase es muy útil para comprender cómo funcionan los subprocesos en los programas Java. Podemos acceder al hilo que se está ejecutando invocando el currentThread().getName()método, como se muestra aquí:

 public class MainThread { public static void main(String... mainThread) { System.out.println(Thread.currentThread().getName()); } } 

Este código imprimirá "principal", identificando el hilo que se está ejecutando actualmente. Saber cómo identificar el hilo que se está ejecutando es el primer paso para absorber los conceptos del hilo.

El ciclo de vida del hilo de Java

Al trabajar con subprocesos, es fundamental conocer el estado del subproceso. El ciclo de vida del hilo de Java consta de seis estados de hilo:

  • Nuevo : se Thread()ha creado una instancia de un nuevo .
  • Ejecutable : El Thread's start()método ha sido invocado.
  • En ejecución : el start()método se ha invocado y el hilo se está ejecutando.
  • Suspendido : el hilo se suspende temporalmente y puede reanudarlo otro hilo.
  • Bloqueado : el hilo está esperando una oportunidad para ejecutarse. Esto sucede cuando un hilo ya ha invocado el synchronized()método y el siguiente hilo debe esperar hasta que finalice.
  • Terminado : la ejecución del hilo está completa.
Rafael Chinelato Del Nero

Hay más para explorar y comprender sobre los estados de los hilos, pero la información de la Figura 1 es suficiente para resolver este desafío de Java.

Procesamiento concurrente: extensión de una clase de subproceso

En su forma más simple, el procesamiento concurrente se realiza extendiendo una Threadclase, como se muestra a continuación.

 public class InheritingThread extends Thread { InheritingThread(String threadName) { super(threadName); } public static void main(String... inheriting) { System.out.println(Thread.currentThread().getName() + " is running"); new InheritingThread("inheritingThread").start(); } @Override public void run() { System.out.println(Thread.currentThread().getName() + " is running"); } } 

Aquí estamos ejecutando dos subprocesos: el MainThready el InheritingThread. Cuando invocamos el start()método con el nuevo inheritingThread(), run()se ejecuta la lógica del método.

También pasamos el nombre del segundo hilo en el Threadconstructor de la clase, por lo que la salida será:

 main is running. inheritingThread is running. 

La interfaz Runnable

En lugar de utilizar la herencia, puede implementar la interfaz Runnable. Pasar Runnabledentro de un Threadconstructor da como resultado menos acoplamiento y más flexibilidad. Después de pasar Runnable, podemos invocar el start()método exactamente como lo hicimos en el ejemplo anterior:

 public class RunnableThread implements Runnable { public static void main(String... runnableThread) { System.out.println(Thread.currentThread().getName()); new Thread(new RunnableThread()).start(); } @Override public void run() { System.out.println(Thread.currentThread().getName()); } } 

Subprocesos non-daemon vs daemon

En términos de ejecución, hay dos tipos de subprocesos:

  • Los subprocesos que no son demonios se ejecutan hasta el final. El hilo principal es un buen ejemplo de hilo que no es demonio. El código de entrada main()siempre se ejecutará hasta el final, a menos que System.exit()obligue al programa a completarse.
  • Un hilo de demonio es lo opuesto, básicamente un proceso que no requiere ser ejecutado hasta el final.

Recuerde la regla : si un subproceso adjunto que no es demonio termina antes que un subproceso demonio, el subproceso demonio no se ejecutará hasta el final.

Para comprender mejor la relación entre los subprocesos daemon y no daemon, estudie este ejemplo:

 import java.util.stream.IntStream; public class NonDaemonAndDaemonThread { public static void main(String... nonDaemonAndDaemon) throws InterruptedException { System.out.println("Starting the execution in the Thread " + Thread.currentThread().getName()); Thread daemonThread = new Thread(() -> IntStream.rangeClosed(1, 100000) .forEach(System.out::println)); daemonThread.setDaemon(true); daemonThread.start(); Thread.sleep(10); System.out.println("End of the execution in the Thread " + Thread.currentThread().getName()); } } 

En este ejemplo, he usado un hilo de demonio para declarar un rango de 1 a 100.000, iterarlos todos y luego imprimir. Pero recuerde, un subproceso daemon no completará la ejecución si el subproceso principal del no demonio termina primero.

La salida procederá de la siguiente manera:

  1. Inicio de ejecución en el hilo principal.
  2. Imprime números desde 1 hasta posiblemente 100.000.
  3. Fin de la ejecución en el hilo principal, muy probablemente antes de que se complete la iteración a 100.000.

El resultado final dependerá de su implementación de JVM.

Y eso me lleva al siguiente punto: los hilos son impredecibles.

Prioridad de subprocesos y JVM

Es posible priorizar la ejecución de subprocesos con el setPrioritymétodo, pero cómo se maneja depende de la implementación de JVM. Linux, MacOS y Windows tienen diferentes implementaciones de JVM, y cada uno manejará la prioridad de los subprocesos de acuerdo con sus propios valores predeterminados.

Sin embargo, la prioridad de subproceso que establezca influye en el orden de invocación de subprocesos. Las tres constantes declaradas en la Threadclase son:

 /** * The minimum priority that a thread can have. */ public static final int MIN_PRIORITY = 1; /** * The default priority that is assigned to a thread. */ public static final int NORM_PRIORITY = 5; /** * The maximum priority that a thread can have. */ public static final int MAX_PRIORITY = 10; 

Try running some tests on the following code to see what execution priority you end up with:

 public class ThreadPriority { public static void main(String... threadPriority) { Thread moeThread = new Thread(() -> System.out.println("Moe")); Thread barneyThread = new Thread(() -> System.out.println("Barney")); Thread homerThread = new Thread(() -> System.out.println("Homer")); moeThread.setPriority(Thread.MAX_PRIORITY); barneyThread.setPriority(Thread.NORM_PRIORITY); homerThread.setPriority(Thread.MIN_PRIORITY); homerThread.start(); barneyThread.start(); moeThread.start(); } } 

Even if we set moeThread as MAX_PRIORITY, we cannot count on this thread being executed first. Instead, the order of execution will be random.

Constants vs enums

The Thread class was introduced with Java 1.0. At that time, priorities were set using constants, not enums. There's a problem with using constants, however: if we pass a priority number that is not in the range of 1 to 10, the setPriority() method will throw an IllegalArgumentException. Today, we can use enums to get around this issue. Using enums makes it impossible to pass an illegal argument, which both simplifies the code and gives us more control over its execution.

Take the Java threads challenge!

You've learned just a little bit about threads, but it's enough for this post's Java challenge.

To start, study the following code:

 public class ThreadChallenge { private static int wolverineAdrenaline = 10; public static void main(String... doYourBest) { new Motorcycle("Harley Davidson").start(); Motorcycle fastBike = new Motorcycle("Dodge Tomahawk"); fastBike.setPriority(Thread.MAX_PRIORITY); fastBike.setDaemon(false); fastBike.start(); Motorcycle yamaha = new Motorcycle("Yamaha YZF"); yamaha.setPriority(Thread.MIN_PRIORITY); yamaha.start(); } static class Motorcycle extends Thread { Motorcycle(String bikeName) { super(bikeName); } @Override public void run() { wolverineAdrenaline++; if (wolverineAdrenaline == 13) { System.out.println(this.getName()); } } } } 

What will be the output of this code? Analyze the code and try to determine the answer for yourself, based on what you've learned.

A. Harley Davidson

B. Dodge Tomahawk

C. Yamaha YZF

D. Indeterminate

What just happened? Understanding threads behavior

In the above code, we created three threads. The first thread is Harley Davidson, and we assigned this thread the default priority. The second thread is Dodge Tomahawk, assigned MAX_PRIORITY. The third is Yamaha YZF, with MIN_PRIORITY. Then we started the threads.

In order to determine the order the threads will run in, you might first note that the Motorcycle class extends the Thread class, and that we've passed the thread name in the constructor. We've also overridden the run() method with a condition: if wolverineAdrenaline is equals to 13.

Even though Yamaha YZF is the third thread in our order of execution, and has MIN_PRIORITY, there's no guarantee that it will be executed last for all JVM implementations.

You might also note that in this example we set the Dodge Tomahawk thread as daemon. Because it's a daemon thread, Dodge Tomahawk may never complete execution. But the other two threads are non-daemon by default, so the Harley Davidson and Yamaha YZF threads will definitely complete their execution.

To conclude, the result will be D: Indeterminate, because there is no guarantee that the thread scheduler will follow our order of execution or thread priority.

Remember, we can't rely on program logic (order of threads or thread priority) to predict the JVM's order of execution.

Video challenge! Debugging variable arguments

Debugging is one of the easiest ways to fully absorb programming concepts while also improving your code. In this video you can follow along while I debug and explain the thread behavior challenge:

Common mistakes with Java threads

  • Invoking the run() method to try to start a new thread.
  • Trying to start a thread twice (this will cause an IllegalThreadStateException).
  • Allowing multiple processes to change the state of an object when it shouldn't change.
  • Writing program logic that relies on thread priority (you can't predict it).
  • Relying on the order of thread execution--even if we start a thread first, there is no guarantee it will be executed first.

What to remember about Java threads

  • Invoke the start() method to start a Thread.
  • It's possible to extend the Thread class directly in order to use threads.
  • It's possible to implement a thread action inside a Runnable interface.
  • Thread priority depends on the JVM implementation.
  • Thread behavior will always depend on the JVM implementation.
  • A daemon thread won't complete if an enclosing non-daemon thread ends first.

Learn more about Java threads on JavaWorld

  • Read the Java 101 threads series to learn more about threads and runnables, thread synchronization, thread scheduling with wait/notify, and thread death.
  • Modern threading: A Java concurrency primer introduces java.util.concurrent and answers common questions for developers new to Java concurrency.
  • Modern threading for not-quite-beginners offers more advanced tips and best practices for working with java.util.concurrent.

More from Rafael

  • Get more quick code tips: Read all the posts in the Java Challengers series.
  • Build your Java skills: Visit the Java Dev Gym for a code workout.
  • Want to work on stress free projects and write bug-free code? Visit the NoBugsProject for your copy of No Bugs, No Stress - Create a Life-Changing Software Without Destroying Your Life.

Esta historia, "Comportamiento de subprocesos en la JVM", fue publicada originalmente por JavaWorld.