Concurrencia en Java

El lenguaje de programación Java y la JVM ( Java Virtual Machine ) están diseñados para soportar computación paralela , y todos los cálculos se realizan en el contexto de un hilo . Múltiples subprocesos pueden compartir objetos y recursos; cada subproceso ejecuta sus propias instrucciones (código), pero potencialmente puede acceder a cualquier objeto en el programa. Es responsabilidad del programador coordinar (o " sincronizar "") subprocesos durante operaciones de lectura y escritura en objetos compartidos. La sincronización de subprocesos es necesaria para garantizar que solo un subproceso pueda acceder a un objeto a la vez y para evitar que los subprocesos accedan a objetos actualizados de forma incompleta mientras otro subproceso está trabajando en ellos. El lenguaje Java tiene construcciones de soporte de sincronización de subprocesos integradas.

Procesos e hilos

La mayoría de las implementaciones de Java Virtual Machine utilizan un solo proceso para ejecutar el programa y, en el lenguaje de programación Java, la computación paralela se asocia más comúnmente con subprocesos . Los hilos a veces se denominan procesos ligeros .

Objetos de flujo

Los subprocesos comparten recursos de proceso, como memoria y archivos abiertos, entre ellos. Este enfoque conduce a una comunicación efectiva pero potencialmente problemática. Cada aplicación tiene al menos un subproceso en ejecución. El hilo desde el que comienza la ejecución del programa se llama main o principal . El subproceso principal puede crear subprocesos adicionales en forma de objetos Runnableo archivos Callable. (La interfaz Callablees similar en el sentido de Runnableque ambos están diseñados para clases de las que se crearán instancias en un subproceso separado. RunnableSin embargo, no devuelve un resultado y no puede generar una excepción verificada ).

Cada subproceso se puede programar para ejecutarse en un núcleo de CPU separado, usar la división de tiempo en un solo núcleo de procesador o usar la división de tiempo en múltiples procesadores. En los dos últimos casos, el sistema cambiará periódicamente entre subprocesos, permitiendo alternativamente que se ejecute uno u otro subproceso. Este esquema se llama pseudo-paralelismo. No existe una solución universal que diga exactamente cómo se convertirán los subprocesos de Java en subprocesos nativos del sistema operativo. Depende de la implementación específica de JVM.

En Java, un subproceso se representa como un objeto secundario del Thread. Esta clase encapsula mecanismos de subprocesos estándar. Los subprocesos se pueden administrar directamente o mediante mecanismos abstractos como Executor y colecciones del paquete java.util.concurrent.

Ejecutar un hilo

Hay dos formas de iniciar un hilo nuevo:

  • Implementación de la interfaz Runnable
La clase pública HelloRunnable implementa Runnable { public void run () { System . fuera _ println ( "¡Hola desde el hilo!" ); } public static void main ( String [] args ) { ( nuevo hilo ( nuevo HelloRunnable ())). inicio (); } }
  • Herencia de la clase Thread
public class HelloThread extiende Thread { public void run () { System . fuera _ println ( "¡Hola desde el hilo!" ); } public static void main ( String [] args ) { ( new HelloThread ()). inicio (); } } Interrupciones

Una interrupción es una indicación para un subproceso de que debe detener el trabajo actual y hacer otra cosa. Un subproceso puede enviar una interrupción llamando al método interrupt() del objeto Threadsi necesita interrumpir su subproceso asociado. El mecanismo de interrupción se implementa utilizando el estado de interrupción de la bandera interna (bandera de interrupción) de la clase Thread. Llamar a Thread.interrupt() levanta esta bandera. Por convención, cualquier método que termine con una InterruptedException restablecerá el indicador de interrupción. Hay dos formas de verificar si esta bandera está configurada. La primera forma es llamar al método bool isInterrupted() del objeto hilo, la segunda forma es llamar al método estático bool Thread.interrupted() . El primer método devuelve el estado de la bandera de interrupción y deja esta bandera intacta. El segundo método devuelve el estado de la bandera y lo restablece. Tenga en cuenta que Thread.interrupted()  es un método estático de la clase Thready llamarlo devuelve el valor del indicador de interrupción del hilo desde el que se llamó.

En espera de finalización

Java proporciona un mecanismo que permite que un subproceso espere a que otro subproceso termine de ejecutarse. Para ello se utiliza el método Thread.join() .

Demonios

En Java, un proceso termina cuando termina su último hilo. Incluso si el método main() ya se completó, pero los subprocesos que generó aún se están ejecutando, el sistema esperará a que se completen. Sin embargo, esta regla no se aplica a un tipo especial de subprocesos: los demonios. Si el último subproceso normal del proceso ha terminado y solo quedan subprocesos daemon, se terminarán a la fuerza y ​​el proceso finalizará. La mayoría de las veces, los subprocesos daemon se utilizan para realizar tareas en segundo plano que dan servicio a un proceso durante su vida útil.

Declarar un subproceso como demonio es bastante simple: debe llamar a su método setDaemon(true) antes de iniciar el subproceso ; Puede comprobar si un subproceso es un demonio llamando a su método booleano isDaemon() .

Excepciones

Una excepción lanzada y no controlada hará que el subproceso finalice. El subproceso principal imprimirá automáticamente la excepción en la consola, y los subprocesos creados por el usuario solo pueden hacerlo registrando un controlador. [1] [2]

Modelo de memoria

El modelo de memoria Java [1] describe la interacción de hilos a través de la memoria en el lenguaje de programación Java. A menudo, en las computadoras modernas, el código no se ejecuta en el orden en que se escribe en aras de la velocidad. La permutación la realizan el compilador , el procesador y el subsistema de memoria . El lenguaje de programación Java no garantiza la atomicidad de las operaciones y la coherencia secuencial al leer o escribir campos de objetos compartidos. Esta solución libera las manos del compilador y permite optimizaciones (como la asignación de registros , la eliminación de subexpresiones comunes y la eliminación de operaciones de lectura redundantes ) basadas en la permutación de las operaciones de acceso a la memoria. [3]

Sincronización

Los subprocesos se comunican compartiendo el acceso a campos y objetos a los que hacen referencia los campos. Esta forma de comunicación es extremadamente eficiente, pero hace posibles dos tipos de errores: interferencia de subprocesos y errores de consistencia de la memoria. Para evitar que ocurran, existe un mecanismo de sincronización.

El reordenamiento (reordering, reordering) se manifiesta en programas de subprocesos múltiples sincronizados incorrectamente , donde un subproceso puede observar los efectos producidos por otros subprocesos, y dichos programas pueden detectar que los valores actualizados de las variables se vuelven visibles para otros subprocesos en un diferente orden que el especificado en el código fuente.

Para sincronizar hilos en Java se utilizan monitores , que son un mecanismo de alto nivel que permite que solo un hilo a la vez ejecute un bloque de código protegido por un monitor. El comportamiento de los monitores se considera en términos de bloqueos ; Cada objeto tiene un candado asociado.

La sincronización tiene varios aspectos. La más conocida es la exclusión mutua : solo un subproceso puede poseer un monitor, por lo que la sincronización en el monitor significa que una vez que un subproceso ingresa a un bloque sincronizado protegido por el monitor, ningún otro subproceso puede ingresar al bloque protegido por este monitor hasta el primer subproceso. sale del bloque sincronizado.

Pero la sincronización es más que una simple exclusión mutua. La sincronización garantiza que los datos escritos en la memoria antes o dentro de un bloque sincronizado sean visibles para otros subprocesos que están sincronizados en el mismo monitor. Después de salir del bloque sincronizado, liberamos el monitor, lo que tiene el efecto de vaciar el caché en la memoria principal para que las escrituras realizadas por nuestro subproceso puedan ser visibles para otros subprocesos. Antes de que podamos ingresar al bloque sincronizado, adquirimos el monitor, lo que tiene el efecto de invalidar el caché del procesador local para que las variables se carguen desde la memoria principal. Luego podemos ver todas las entradas visibles por la versión anterior del monitor. (JSR 133)

Una lectura-escritura en un campo es una operación atómica si el campo se declara volátil o está protegido por un bloqueo único adquirido antes de cualquier lectura-escritura.

Bloqueos y bloques sincronizados

El efecto de la exclusión mutua y la sincronización de subprocesos se logra ingresando un bloque o método sincronizado que adquiere el bloqueo implícitamente, o adquiriendo el bloqueo explícitamente (como ReentrantLockdel paquete java.util.concurrent.locks). Ambos enfoques tienen el mismo efecto sobre el comportamiento de la memoria. Si todos los intentos de acceso a un determinado campo están protegidos por el mismo bloqueo, las operaciones de lectura y escritura de este campo son atómicas .

Campos volátiles

Cuando se aplica a los campos, la palabra clave volatilegarantiza:

  1. (En todas las versiones de Java) Los accesos a volatile-variable se ordenan globalmente. Esto significa que cada subproceso que acceda al volatilecampo leerá su valor antes de continuar, en lugar de (si es posible) usar el valor almacenado en caché. ( volatileLos accesos a las variables no se pueden reordenar entre sí, pero se pueden reordenar con los accesos a las variables ordinarias. Esto niega la utilidad volatilede los campos como medio de señalización de un subproceso a otro).
  2. (En Java 5 y posterior) Escribir en un campo tiene volatileel mismo efecto en la memoria que la liberación del monitor , mientras que la lectura tiene el mismo efecto que la adquisición del monitor .  Al acceder al campo se establece la relación " sucede antes " . [4] Esencialmente, esta relación es una garantía de que todo lo que era visible para el subproceso cuando escribió en el campo -se vuelve visible para el subproceso cuando lee . volatile AvolatilefBf

Volatile-los campos son atómicos. Leer desde volatileun campo tiene el mismo efecto que adquirir un bloqueo: los datos en la memoria de trabajo se declaran inválidos y el volatilevalor del campo se vuelve a leer de la memoria. Escribir en un volatilecampo tiene el mismo efecto en la memoria que liberar un bloqueo: volatileel campo se escribe inmediatamente en la memoria.

Campos finales

Un campo que se declara final se llama final y no se puede cambiar después de la inicialización. Los campos finales de un objeto se inicializan en su constructor. Si el constructor sigue ciertas reglas simples, el valor correcto del campo final será visible para otros subprocesos sin sincronización. Una regla simple es que la referencia this no debe abandonar el constructor hasta que se haya completado.

Historia

A partir de JDK 1.2 , Java incluye un conjunto estándar de clases de colección de Java Collections Framework .

Doug Lee , quien también contribuyó a la implementación de Java Collections Framework, desarrolló el paquete de concurrencia , que incluye varias primitivas de sincronización y una gran cantidad de clases relacionadas con la colección. [5] Se continuó trabajando en él como parte de JSR 166 [6] bajo la presidencia de Doug Lee .

El lanzamiento de JDK 5.0 incluyó muchas adiciones y aclaraciones al modelo de concurrencia de Java. Por primera vez, las API de concurrencia desarrolladas por JSR 166 se incluyeron en el JDK. JSR 133 proporcionó soporte para operaciones atómicas bien definidas en un entorno multiproceso/multiprocesador.

Tanto Java SE 6 como Java SE 7 traen cambios y adiciones a la API JSR 166.

Véase también

Notas

  1. ↑ Interfaz de Oracle Thread.UncaughtExceptionHandler . Consultado el 10 de mayo de 2014. Archivado desde el original el 12 de mayo de 2014.
  2. Muerte de Silent Thread debido a excepciones no controladas . readjava.com . Consultado el 10 de mayo de 2014. Archivado desde el original el 12 de mayo de 2014.
  3. Herlihy, Maurice y Nir Shavit. "El arte de la programación multiprocesador". PODC. vol. 6. 2006.
  4. Sección 17.4.4: Orden de sincronización The Java® Language Specification, Java SE 7 Edition . Corporación Oracle (2013). Consultado el 12 de mayo de 2013. Archivado desde el original el 3 de febrero de 2021.
  5. Doug Lee . Descripción general del paquete util.concurrent Versión 1.3.4 . — « Nota: Tras el lanzamiento de J2SE 5.0, este paquete entra en modo de mantenimiento: solo se publicarán las correcciones esenciales. El paquete J2SE5 java.util.concurrent incluye versiones mejoradas, más eficientes y estandarizadas de los componentes principales de este paquete. ". Consultado el 1 de enero de 2011. Archivado desde el original el 18 de diciembre de 2020.
  6. JSR 166: Utilidades de concurrencia (enlace no disponible) . Consultado el 3 de noviembre de 2015. Archivado desde el original el 3 de noviembre de 2016. 

Enlaces

  • Goetz, Brian; Josué Bloch; José Bowbeer; doug lea; david holmes Tim Peierls. Concurrencia de Java en la práctica  (neopr.) . -Addison Wesley , 2006. -ISBN 0-321-34960-1 .
  • Lea, Doug. Programación Concurrente en Java : Principios y Patrones de Diseño  . -Addison Wesley , 1999. -ISBN 0-201-31009-0 .

Enlaces a recursos externos