Java 101: comprensión de los subprocesos de Java, parte 3: programación de subprocesos y espera / notificación

Este mes, continúo mi introducción de cuatro partes a los subprocesos de Java centrándome en la programación de subprocesos, el mecanismo de espera / notificación y la interrupción de subprocesos. Investigará cómo una JVM o un programador de subprocesos del sistema operativo elige el siguiente subproceso para su ejecución. Como descubrirá, la prioridad es importante para la elección de un programador de subprocesos. Examinará cómo un subproceso espera hasta que recibe una notificación de otro subproceso antes de continuar la ejecución y aprenderá a utilizar el mecanismo de espera / notificación para coordinar la ejecución de dos subprocesos en una relación productor-consumidor. Finalmente, aprenderá cómo despertar prematuramente un hilo durmiente o en espera para la terminación del hilo u otras tareas. También te enseñaré cómo un hilo que no está inactivo ni esperando detecta una solicitud de interrupción de otro hilo.

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, espera / notificación e interrupción de subprocesos
  • Parte 4: grupos de subprocesos, volatilidad, variables locales de subprocesos, temporizadores y muerte de subprocesos

Programación de hilos

En un mundo idealizado, todos los subprocesos del programa tendrían sus propios procesadores en los que ejecutarse. Hasta que llegue el momento en que las computadoras tengan miles o millones de procesadores, los subprocesos a menudo deben compartir uno o más procesadores. La JVM o el sistema operativo de la plataforma subyacente descifran cómo compartir el recurso del procesador entre subprocesos, una tarea conocida como programación de subprocesos . La parte de la JVM o del sistema operativo que realiza la programación de subprocesos es un programador de subprocesos .

Nota: Para simplificar mi discusión sobre la programación de subprocesos, me centro en la programación de subprocesos en el contexto de un solo procesador. Puede extrapolar esta discusión a varios procesadores; Te dejo esa tarea.

Recuerde dos puntos importantes sobre la programación de subprocesos:

  1. Java no obliga a una máquina virtual a programar subprocesos de una manera específica ni contiene un programador de subprocesos. Eso implica una programación de subprocesos dependiente de la plataforma. Por lo tanto, debe tener cuidado al escribir un programa Java cuyo comportamiento depende de cómo se programan los subprocesos y debe operar de manera consistente en diferentes plataformas.
  2. Afortunadamente, al escribir programas Java, debe pensar en cómo Java programa los subprocesos solo cuando al menos uno de los subprocesos de su programa utiliza mucho el procesador durante largos períodos de tiempo y los resultados intermedios de la ejecución de ese subproceso resultan importantes. Por ejemplo, un subprograma contiene un hilo que crea dinámicamente una imagen. Periódicamente, desea que el hilo de pintura dibuje el contenido actual de esa imagen para que el usuario pueda ver cómo progresa la imagen. Para asegurarse de que el hilo de cálculo no monopolice el procesador, considere la programación de hilos.

Examine un programa que crea dos subprocesos de procesador intensivo:

Listado 1. SchedDemo.java

// SchedDemo.java class SchedDemo { public static void main (String [] args) { new CalcThread ("CalcThread A").start (); new CalcThread ("CalcThread B").start (); } } class CalcThread extends Thread { CalcThread (String name) { // Pass name to Thread layer. super (name); } double calcPI () { boolean negative = true; double pi = 0.0; 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; return pi; } public void run () { for (int i = 0; i < 5; i++) System.out.println (getName () + ": " + calcPI ()); } }

SchedDemocrea dos hilos que calculan cada uno el valor de pi (cinco veces) e imprimen cada resultado. Dependiendo de cómo su implementación de JVM programe los subprocesos, es posible que vea un resultado similar al siguiente:

CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894

Según el resultado anterior, el programador de subprocesos comparte el procesador entre ambos subprocesos. Sin embargo, puede ver un resultado similar a este:

CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894

La salida anterior muestra que el programador de subprocesos favorece a un subproceso sobre otro. Las dos salidas anteriores ilustran dos categorías generales de programadores de subprocesos: verde y nativo. Exploraré sus diferencias de comportamiento en las próximas secciones. Al discutir cada categoría, me refiero a los estados de los hilos, de los cuales hay cuatro:

  1. Estado inicial: un programa ha creado un objeto hilo de hilo, pero el hilo todavía no existe porque el start()método del objeto hilo todavía no ha sido llamado.
  2. Estado ejecutable: este es el estado predeterminado de un hilo. Después de que se start()completa la llamada a , un hilo se vuelve ejecutable si ese hilo se está ejecutando o no, es decir, usando el procesador. Aunque se pueden ejecutar muchos subprocesos, actualmente solo se ejecuta uno. Los programadores de subprocesos determinan qué subproceso ejecutable asignar al procesador.
  3. Estado bloqueado: Cuando un hilo ejecuta las sleep(), wait()o join()métodos, cuando un intento de hilo para leer datos aún no disponibles en una red, y cuando un hilo espera para obtener un bloqueo, ese hilo está en el estado bloqueado: no es ni corriente ni en posición de correr. (Probablemente pueda pensar en otras ocasiones en las que un hilo esperaría a que sucediera algo). Cuando un hilo bloqueado se desbloquea, ese hilo pasa al estado ejecutable.
  4. Estado de terminación: una vez que la ejecución abandona el run()método de un hilo , ese hilo está en el estado de terminación. En otras palabras, el hilo deja de existir.

¿Cómo elige el programador de subprocesos qué subproceso ejecutable ejecutar? Empiezo a responder esa pregunta mientras hablo de la programación del hilo verde. Termino la respuesta mientras hablo de la programación de hilos nativos.

Programación de hilo verde

No todos los sistemas operativos, el antiguo sistema operativo Microsoft Windows 3.1, por ejemplo, admiten subprocesos. Para tales sistemas, Sun Microsystems puede diseñar una JVM que divide su único hilo de ejecución en varios hilos. La JVM (no el sistema operativo de la plataforma subyacente) proporciona la lógica de subprocesos y contiene el planificador de subprocesos. Los subprocesos de JVM son subprocesos verdes o subprocesos de usuario .

El programador de subprocesos de una JVM programa los subprocesos verdes de acuerdo con la prioridad , la importancia relativa de un subproceso, que se expresa como un número entero a partir de un rango de valores bien definido. Por lo general, el programador de subprocesos de una JVM elige el subproceso de mayor prioridad y permite que ese subproceso se ejecute hasta que termine o se bloquee. En ese momento, el programador de subprocesos elige un subproceso de la siguiente prioridad más alta. Ese hilo (generalmente) se ejecuta hasta que termina o se bloquea. Si, mientras se ejecuta un subproceso, se desbloquea un subproceso de mayor prioridad (quizás el tiempo de suspensión del subproceso de mayor prioridad expiró), el programador de subprocesos se adelanta, o interrumpe, el subproceso de menor prioridad y asigna el subproceso de mayor prioridad desbloqueado al procesador.

Nota: un hilo ejecutable con la prioridad más alta no siempre se ejecutará. Aquí está la prioridad de la especificación del lenguaje Java :

Cada hilo tiene una prioridad. Cuando hay competencia por los recursos de procesamiento, los subprocesos con mayor prioridad generalmente se ejecutan con preferencia a los subprocesos con menor prioridad. Sin embargo, tal preferencia no es una garantía de que el subproceso de mayor prioridad siempre se esté ejecutando, y las prioridades del subproceso no se pueden usar para implementar de manera confiable la exclusión mutua.

Esa admisión dice mucho sobre la implementación de JVM de hilo verde. Esas JVM no pueden permitirse el lujo de permitir que los subprocesos se bloqueen porque eso ataría el único subproceso de ejecución de la JVM. Por lo tanto, cuando un subproceso debe bloquearse, como cuando ese subproceso lee datos con lentitud para llegar de un archivo, la JVM puede detener la ejecución del subproceso y utilizar un mecanismo de sondeo para determinar cuándo llegan los datos. Mientras el subproceso permanece detenido, el programador de subprocesos de la JVM puede programar la ejecución de un subproceso de menor prioridad. Supongamos que llegan datos mientras se está ejecutando el subproceso de menor prioridad. Aunque el subproceso de mayor prioridad debería ejecutarse tan pronto como lleguen los datos, eso no sucede hasta que la JVM sondea el sistema operativo y descubre la llegada. Por lo tanto, el subproceso de menor prioridad se ejecuta aunque se deba ejecutar el subproceso de mayor prioridad.Debe preocuparse por esta situación solo cuando necesite un comportamiento en tiempo real de Java. Pero Java no es un sistema operativo en tiempo real, ¿por qué preocuparse?

Para comprender qué hilo verde ejecutable se convierte en el hilo verde que se ejecuta actualmente, considere lo siguiente. Suponga que su aplicación consta de tres subprocesos: el subproceso principal que ejecuta el main()método, un subproceso de cálculo y un subproceso que lee la entrada del teclado. Cuando no hay entrada de teclado, el hilo de lectura se bloquea. Suponga que el hilo de lectura tiene la prioridad más alta y el hilo de cálculo tiene la prioridad más baja. (En aras de la simplicidad, suponga también que no hay otros subprocesos JVM internos disponibles). La Figura 1 ilustra la ejecución de estos tres subprocesos.

En el momento T0, el hilo principal comienza a ejecutarse. En el momento T1, el hilo principal inicia el hilo de cálculo. Debido a que el subproceso de cálculo tiene una prioridad menor que el subproceso principal, el subproceso de cálculo espera al procesador. En el momento T2, el hilo principal inicia el hilo de lectura. Debido a que el hilo de lectura tiene una prioridad más alta que el hilo principal, el hilo principal espera al procesador mientras se ejecuta el hilo de lectura. En el momento T3, el hilo de lectura se bloquea y se ejecuta el hilo principal. En el momento T4, el hilo de lectura se desbloquea y se ejecuta; el hilo principal espera. Finalmente, en el momento T5, el hilo de lectura se bloquea y se ejecuta el hilo principal. Esta alternancia en la ejecución entre la lectura y los hilos principales continúa mientras se ejecuta el programa. El hilo de cálculo nunca se ejecuta porque tiene la prioridad más baja y, por lo tanto, no recibe atención del procesador.una situación conocida comohambre del procesador .

Podemos alterar este escenario dando al hilo de cálculo la misma prioridad que al hilo principal. La Figura 2 muestra el resultado, comenzando con el tiempo T2. (Antes de T2, la Figura 2 es idéntica a la Figura 1.)

En el tiempo T2, el hilo de lectura se ejecuta mientras que los hilos principal y de cálculo esperan al procesador. En el momento T3, el hilo de lectura se bloquea y el hilo de cálculo se ejecuta, porque el hilo principal se ejecutó justo antes del hilo de lectura. En el momento T4, el hilo de lectura se desbloquea y se ejecuta; los hilos principal y de cálculo esperan. En el momento T5, el hilo de lectura se bloquea y se ejecuta el hilo principal, porque el hilo de cálculo se ejecutó justo antes del hilo de lectura. Esta alternancia en la ejecución entre los subprocesos principal y de cálculo continúa mientras el programa se ejecuta y depende de la ejecución y el bloqueo del subproceso de mayor prioridad.

We must consider one last item in green thread scheduling. What happens when a lower-priority thread holds a lock that a higher-priority thread requires? The higher-priority thread blocks because it cannot get the lock, which implies that the higher-priority thread effectively has the same priority as the lower-priority thread. For example, a priority 6 thread attempts to acquire a lock that a priority 3 thread holds. Because the priority 6 thread must wait until it can acquire the lock, the priority 6 thread ends up with a 3 priority—a phenomenon known as priority inversion.

La inversión de prioridad puede retrasar en gran medida la ejecución de un subproceso de mayor prioridad. Por ejemplo, suponga que tiene tres subprocesos con prioridades de 3, 4 y 9. El subproceso de prioridad 3 se está ejecutando y los otros subprocesos están bloqueados. Suponga que el subproceso de prioridad 3 se bloquea y el subproceso de prioridad 4 se desbloquea. El hilo de prioridad 4 se convierte en el hilo que se está ejecutando actualmente. Debido a que el subproceso de prioridad 9 requiere el bloqueo, continúa esperando hasta que el subproceso de prioridad 3 libere el bloqueo. Sin embargo, el hilo de prioridad 3 no puede liberar el bloqueo hasta que el hilo de prioridad 4 se bloquee o termine. Como resultado, el hilo de prioridad 9 retrasa su ejecución.