Java 101: comprensión de los subprocesos de Java, parte 1: introducción de subprocesos y ejecutables

Este artículo es el primero de una serie Java 101 de cuatro partes que explora los hilos de Java. Aunque podría pensar que los subprocesos en Java serían difíciles de entender, pretendo mostrarles que los subprocesos son fáciles de entender. En este artículo, te presento a los subprocesos y ejecutables de Java. En artículos posteriores, exploraremos la sincronización (a través de bloqueos), los problemas de sincronización (como el punto muerto), el mecanismo de espera / notificación, la programación (con y sin prioridad), la interrupción de subprocesos, los temporizadores, la volatilidad, los grupos de subprocesos y las variables locales de subprocesos. .

Tenga en cuenta que este artículo (parte de los archivos de JavaWorld) se actualizó con nuevas listas de código y código fuente descargable en mayo de 2013.

Comprensión de los hilos de Java: lea la serie completa

  • Parte 1: Introducción a subprocesos y ejecutables
  • Parte 2: Sincronización
  • Parte 3: programación de subprocesos y espera / notificación
  • Parte 4: Grupos de hilos y volatilidad

¿Qué es un hilo?

Conceptualmente, la noción de hilo no es difícil de comprender: es una ruta de ejecución independiente a través del código del programa. Cuando se ejecutan varios subprocesos, la ruta de un subproceso a través del mismo código generalmente difiere de los demás. Por ejemplo, suponga que un subproceso ejecuta el código de bytes equivalente a la parte de una instrucción if-else if, mientras que otro subproceso ejecuta el código de bytes equivalente a la elseparte. ¿Cómo realiza la JVM un seguimiento de la ejecución de cada hilo? La JVM le da a cada hilo su propia pila de llamadas a métodos. Además de rastrear la instrucción de código de bytes actual, la pila de llamadas al método rastrea las variables locales, los parámetros que la JVM pasa a un método y el valor de retorno del método.

Cuando varios subprocesos ejecutan secuencias de instrucciones de código de bytes en el mismo programa, esa acción se conoce como subprocesos múltiples . El subproceso múltiple beneficia a un programa de varias maneras:

  • Los programas basados ​​en GUI (interfaz gráfica de usuario) multiproceso siguen respondiendo a los usuarios mientras realizan otras tareas, como repaginar o imprimir un documento.
  • Los programas con subprocesos suelen finalizar más rápido que sus homólogos sin subprocesos. Esto es especialmente cierto en los subprocesos que se ejecutan en una máquina multiprocesador, donde cada subproceso tiene su propio procesador.

Java realiza múltiples subprocesos a través de su java.lang.Threadclase. Cada Threadobjeto describe un único hilo de ejecución. Esa ejecución ocurre en Threadel run()método de. Debido a que el run()método predeterminado no hace nada, debe crear una subclase Thready anular run()para lograr un trabajo útil. Para una muestra de subprocesos y subprocesos múltiples en el contexto de Thread, examine el Listado 1:

Listado 1. ThreadDemo.java

// ThreadDemo.java class ThreadDemo { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); for (int i = 0; i < 50; i++) System.out.println ("i = " + i + ", i * i = " + i * i); } } class MyThread extends Thread { public void run () { for (int count = 1, row = 1; row < 20; row++, count++) { for (int i = 0; i < count; i++) System.out.print ('*'); System.out.print ('\n'); } } }

El Listado 1 presenta el código fuente de una aplicación que consta de clases ThreadDemoy MyThread. La clase ThreadDemoimpulsa la aplicación creando un MyThreadobjeto, iniciando un hilo que se asocia con ese objeto y ejecutando algún código para imprimir una tabla de cuadrados. Por el contrario, MyThreadanula Threadel run()método de imprimir (en el flujo de salida estándar) un triángulo en ángulo recto compuesto por caracteres de asterisco.

Programación de subprocesos y JVM

La mayoría (si no todas) las implementaciones de JVM utilizan las capacidades de subprocesamiento de la plataforma subyacente. Debido a que esas capacidades son específicas de la plataforma, el orden de salida de sus programas multiproceso puede diferir del orden de salida de otra persona. Esa diferencia resulta de la programación, un tema que exploro más adelante en esta serie.

Cuando escribe java ThreadDemopara ejecutar la aplicación, la JVM crea un hilo inicial de ejecución, que ejecuta el main()método. Al ejecutar mt.start ();, el hilo de inicio le dice a la JVM que cree un segundo hilo de ejecución que ejecuta las instrucciones de código de bytes que comprenden el método MyThreaddel objeto run(). Cuando el start()método regresa, el hilo inicial ejecuta su forciclo para imprimir una tabla de cuadrados, mientras que el nuevo hilo ejecuta el run()método para imprimir el triángulo en ángulo recto.

¿Cómo se ve la salida? Corre ThreadDemopara averiguarlo. Notarás que la salida de cada hilo tiende a intercalar con la salida del otro. Eso se debe a que ambos subprocesos envían su salida al mismo flujo de salida estándar.

La clase Thread

Para dominar la escritura de código multiproceso, primero debe comprender los diversos métodos que componen la Threadclase. Esta sección explora muchos de esos métodos. Específicamente, aprenderá sobre métodos para iniciar hilos, nombrar hilos, poner hilos en reposo, determinar si un hilo está vivo, unir un hilo a otro hilo y enumerar todos los hilos activos en el grupo y subgrupos de hilos del hilo actual. También hablo de Threadlas ayudas de depuración y los hilos del usuario frente a los hilos del demonio.

Presentaré el resto de Threadlos métodos de 'en artículos posteriores, con la excepción de los métodos obsoletos de Sun.

Métodos obsoletos

Sun ha desaprobado una variedad de Threadmétodos, como suspend()y resume(), porque pueden bloquear sus programas o dañar objetos. Como resultado, no debe llamarlos en su código. Consulte la documentación del SDK para encontrar soluciones a esos métodos. No cubro los métodos obsoletos en esta serie.

Construyendo hilos

Threadtiene ocho constructores. Los mas simples son:

  • Thread(), que crea un Threadobjeto con un nombre predeterminado
  • Thread(String name), que crea un Threadobjeto con un nombre que nameespecifica el argumento

Los siguientes constructores más simples son Thread(Runnable target)y Thread(Runnable target, String name). Aparte de los Runnableparámetros, esos constructores son idénticos a los constructores antes mencionados. La diferencia: los Runnableparámetros identifican objetos externos Threadque proporcionan los run()métodos. (Se aprende acerca Runnablemás adelante en este artículo.) Los últimos cuatro constructores asemejan Thread(String name), Thread(Runnable target)y Thread(Runnable target, String name); sin embargo, los constructores finales también incluyen un ThreadGroupargumento con fines organizativos.

Uno de los últimos cuatro constructores, Thread(ThreadGroup group, Runnable target, String name, long stackSize)es interesante porque le permite especificar el tamaño deseado de la pila de llamadas al método del hilo. Ser capaz de especificar ese tamaño resulta útil en programas con métodos que utilizan la recursividad, una técnica de ejecución mediante la cual un método se llama a sí mismo repetidamente, para resolver elegantemente ciertos problemas. Al establecer explícitamente el tamaño de la pila, a veces puede evitar StackOverflowErrors. Sin embargo, un tamaño demasiado grande puede resultar en OutOfMemoryErrors. Además, Sun considera que el tamaño de la pila de llamadas al método depende de la plataforma. Dependiendo de la plataforma, el tamaño de la pila de llamadas al método puede cambiar. Por lo tanto, piense detenidamente en las ramificaciones de su programa antes de escribir código que llame Thread(ThreadGroup group, Runnable target, String name, long stackSize).

Arranque sus vehículos

Los hilos se parecen a los vehículos: mueven los programas de principio a fin. Thready los Threadobjetos de subclase no son hilos. En cambio, describen los atributos de un hilo, como su nombre, y contienen código (a través de un run()método) que ejecuta el hilo. Cuando llega el momento de ejecutar un nuevo hilo run(), otro hilo llama al método Threaddel objeto de su subclase o del start(). Por ejemplo, para iniciar un segundo subproceso, el subproceso de inicio de la aplicación, que se ejecuta, main()llama start(). En respuesta, el código de manejo de subprocesos de la JVM funciona con la plataforma para garantizar que el subproceso se inicialice correctamente y llame Threadal run()método de un objeto de su subclase .

Una vez que se start()completa, se ejecutan varios subprocesos. Debido a que tendemos a pensar de manera lineal, a menudo nos resulta difícil comprender la actividad concurrente (simultánea) que se produce cuando se ejecutan dos o más subprocesos. Por lo tanto, debe examinar un gráfico que muestre dónde se está ejecutando un hilo (su posición) frente al tiempo. La siguiente figura presenta un gráfico de este tipo.

El gráfico muestra varios períodos de tiempo significativos:

  • La inicialización del hilo inicial
  • En el momento en que ese hilo comienza a ejecutarse main()
  • En el momento en que ese hilo comienza a ejecutarse start()
  • El momento start()crea un nuevo hilo y vuelve amain()
  • La inicialización del nuevo hilo
  • El momento en que el nuevo hilo comienza a ejecutarse run()
  • Los diferentes momentos que termina cada hilo

Tenga en cuenta que la inicialización del nuevo hilo, su ejecución run()y su terminación ocurren simultáneamente con la ejecución del hilo inicial. También tenga en cuenta que después de las llamadas de un hilo start(), las llamadas posteriores a ese método antes de que el run()método salga hacen start()que se arroje un java.lang.IllegalThreadStateExceptionobjeto.

¿Lo que hay en un nombre?

Durante una sesión de depuración, distinguir un hilo de otro de una manera fácil de usar resulta útil. Para diferenciar entre subprocesos, Java asocia un nombre con un subproceso. El nombre predeterminado es Thread, un carácter de guión y un número entero de base cero. Puede aceptar los nombres de subprocesos predeterminados de Java o puede elegir uno propio. Para acomodar nombres personalizados, Threadproporciona constructores que toman nameargumentos y un setName(String name)método. Threadtambién proporciona un getName()método que devuelve el nombre actual. El Listado 2 demuestra cómo establecer un nombre personalizado a través del Thread(String name)constructor y recuperar el nombre actual en el run()método llamando getName():

Listado 2. NameThatThread.java

// NameThatThread.java class NameThatThread { public static void main (String [] args) { MyThread mt; if (args.length == 0) mt = new MyThread (); else mt = new MyThread (args [0]); mt.start (); } } class MyThread extends Thread { MyThread () { // The compiler creates the byte code equivalent of super (); } MyThread (String name) { super (name); // Pass name to Thread superclass } public void run () { System.out.println ("My name is: " + getName ()); } }

Puede pasar un argumento de nombre opcional a MyThreaden la línea de comando. Por ejemplo, java NameThatThread Xestablece Xcomo nombre del hilo. Si no especifica un nombre, verá el siguiente resultado:

My name is: Thread-1

Si lo prefiere, puede cambiar la super (name);llamada en el MyThread (String name)constructor a una llamada a setName (String name)—como en setName (name);. Esta última llamada al método logra el mismo objetivo, establecer el nombre del hilo, que super (name);. Te dejo eso como un ejercicio.

Nombre principal

Java asigna el nombre mainal hilo que ejecuta el main()método, el hilo de inicio. Por lo general, ve ese nombre en el Exception in thread "main"mensaje que imprime el manejador de excepciones predeterminado de la JVM cuando el subproceso inicial arroja un objeto de excepción.

Dormir o no dormir

Later in this column, I will introduce you to animation— repeatedly drawing on one surface images that slightly differ from each other to achieve a movement illusion. To accomplish animation, a thread must pause during its display of two consecutive images. Calling Thread's static sleep(long millis) method forces a thread to pause for millis milliseconds. Another thread could possibly interrupt the sleeping thread. If that happens, the sleeping thread awakes and throws an InterruptedException object from the sleep(long millis) method. As a result, code that calls sleep(long millis) must appear within a try block—or the code's method must include InterruptedException in its throws clause.

Para demostrarlo sleep(long millis), escribí una CalcPI1solicitud. Esa aplicación inicia un nuevo hilo que usa un algoritmo matemático para calcular el valor de la constante matemática pi. Mientras se calcula el nuevo hilo, el hilo inicial se detiene durante 10 milisegundos llamando sleep(long millis). Después de que el hilo inicial se despierta, imprime el valor pi, que el nuevo hilo almacena en variable pi. El Listado 3 presenta CalcPI1el código fuente:

Listado 3. CalcPI1.java

// CalcPI1.java class CalcPI1 { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); try { Thread.sleep (10); // Sleep for 10 milliseconds } catch (InterruptedException e) { } System.out.println ("pi = " + mt.pi); } } class MyThread extends Thread { boolean negative = true; double pi; // Initializes to 0.0, by default public void run () { for (int i = 3; i < 100000; i += 2) { if (negative) pi -= (1.0 / i); else pi += (1.0 / i); negative = !negative; } pi += 1.0; pi *= 4.0; System.out.println ("Finished calculating PI"); } }

Si ejecuta este programa, verá una salida similar (pero probablemente no idéntica) a la siguiente:

pi = -0.2146197014017295 Finished calculating PI