stringtranslate.com

Concurrencia en Java

El lenguaje de programación Java y la máquina virtual Java (JVM) están diseñados para soportar programación concurrente . Toda ejecución tiene lugar en el contexto de subprocesos . Los objetos y recursos pueden ser accedidos por muchos subprocesos separados. Cada subproceso tiene su propia ruta de ejecución, pero potencialmente puede acceder a cualquier objeto en el programa. El programador debe asegurarse de que el acceso de lectura y escritura a los objetos esté correctamente coordinado (o " sincronizado ") entre subprocesos. [1] [2] La sincronización de subprocesos asegura que los objetos sean modificados por un solo subproceso a la vez y evita que los subprocesos accedan a objetos parcialmente actualizados durante la modificación por otro subproceso. [2] El lenguaje Java tiene construcciones integradas para soportar esta coordinación.

Procesos y subprocesos

La mayoría de las implementaciones de la máquina virtual Java se ejecutan como un único proceso . En el lenguaje de programación Java, la programación concurrente se ocupa principalmente de los subprocesos (también llamados procesos ligeros ). Solo se pueden implementar múltiples procesos con múltiples JVM.

Objetos de hilo

Los subprocesos comparten los recursos del proceso, incluida la memoria y los archivos abiertos. Esto permite una comunicación eficiente, pero potencialmente problemática. [2] Cada aplicación tiene al menos un subproceso llamado subproceso principal. El subproceso principal tiene la capacidad de crear subprocesos adicionales como objetos Runnableo Callable. La Callableinterfaz es similar a Runnableen el sentido de que ambos están diseñados para clases cuyas instancias son potencialmente ejecutadas por otro subproceso. [3] Sin embargo, un Runnable, no devuelve un resultado y no puede lanzar una excepción comprobada. [4]

Cada subproceso se puede programar [5] en un núcleo de CPU diferente [6] o utilizar la segmentación de tiempo en un solo procesador de hardware, o la segmentación de tiempo en muchos procesadores de hardware. No existe una solución general para la forma en que los subprocesos de Java se asignan a los subprocesos del sistema operativo nativo. Cada implementación de JVM puede hacer esto de manera diferente.

Cada hilo está asociado a una instancia de la clase Thread. Los hilos se pueden gestionar directamente mediante el uso de los Threadobjetos o indirectamente mediante el uso de mecanismos abstractos como Executors o Tasks. [7]

Iniciar un hilo

Dos formas de iniciar un hilo:

Proporcionar un objeto ejecutable
 clase pública HelloRunnable implementa Runnable { @Override public void run () { System . out . println ( "¡Hola desde el hilo!" ); } public static void main ( String [] args ) { ( new Thread ( new HelloRunnable ())). start (); } }                       
Hilo de subclase
 clase pública HelloThread extiende Thread { @Override void público run () { System . out . println ( "¡Hola desde el hilo!" ); } void público estático main ( String [] args ) { ( new HelloThread ()). start (); } }                      

Interrupciones

Una interrupción le dice a un hilo que debe detener lo que está haciendo y hacer otra cosa. Un hilo envía una interrupción invocando interrupt()en el Threadobjeto para que el hilo sea interrumpido. El mecanismo de interrupción se implementa utilizando un booleanindicador interno conocido como "estado interrumpido". [8] La invocación interrupt()establece este indicador. [9] Por convención, cualquier método que sale lanzando un InterruptedExceptionborra el estado interrumpido cuando lo hace. Sin embargo, siempre es posible que el estado interrumpido se establezca de nuevo inmediatamente, por otro hilo que invoque interrupt().

Se une

El java.lang.Thread#join()método permite Threadesperar la finalización de otro.

Excepciones

Las excepciones no detectadas lanzadas por el código terminarán el hilo. El hilo principal imprime excepciones en la consola, pero los hilos creados por el usuario necesitan un controlador registrado para hacerlo. [10] [11]

Modelo de memoria

El modelo de memoria de Java describe cómo interactúan los hilos en el lenguaje de programación Java a través de la memoria. En las plataformas modernas, el código con frecuencia no se ejecuta en el orden en que fue escrito. Es reordenado por el compilador , el procesador y el subsistema de memoria para lograr el máximo rendimiento. El lenguaje de programación Java no garantiza la linealización , o incluso la consistencia secuencial , [12] al leer o escribir campos de objetos compartidos, y esto es para permitir optimizaciones del compilador (como la asignación de registros , la eliminación de subexpresiones comunes y la eliminación de lecturas redundantes ), todas las cuales funcionan reordenando las lecturas y escrituras de la memoria. [13]

Sincronización

Los subprocesos se comunican principalmente compartiendo el acceso a los campos y a los objetos a los que hacen referencia los campos. Esta forma de comunicación es extremadamente eficiente, pero permite dos tipos de errores: interferencias de subprocesos y errores de consistencia de memoria. La herramienta necesaria para evitar estos errores es la sincronización.

Las reordenaciones pueden entrar en juego en programas multiproceso sincronizados incorrectamente , donde un subproceso puede observar los efectos de otros subprocesos y puede detectar que los accesos a variables se vuelven visibles para otros subprocesos en un orden diferente al ejecutado o especificado en el programa. La mayoría de las veces, a un subproceso no le importa lo que hace el otro. Pero cuando sí le importa, para eso está la sincronización.

Para sincronizar subprocesos, Java utiliza monitores , que son un mecanismo de alto nivel que permite que solo un subproceso a la vez ejecute una región de código protegida por el monitor. El comportamiento de los monitores se explica en términos de bloqueos ; hay un bloqueo asociado con cada objeto.

La sincronización tiene varios aspectos. El más conocido es la exclusión mutua : solo un subproceso puede contener un monitor a la vez, por lo que sincronizar en un monitor significa que una vez que un subproceso ingresa a un bloque sincronizado protegido por un monitor, ningún otro subproceso puede ingresar a un bloque protegido por ese monitor hasta que el primer subproceso salga del bloque sincronizado. [2]

Pero la sincronización implica mucho más que la exclusión mutua. La sincronización garantiza que las escrituras en la memoria realizadas por un subproceso antes o durante un bloque sincronizado se hagan visibles de manera predecible para otros subprocesos que se sincronizan en el mismo monitor. Después de salir de un bloque sincronizado, liberamos el monitor, lo que tiene el efecto de vaciar la memoria caché a la memoria principal, de modo que las escrituras realizadas por este subproceso puedan ser visibles para otros subprocesos. Antes de poder ingresar a un bloque sincronizado, adquirimos el monitor, lo que tiene el efecto de invalidar la memoria caché del procesador local de modo que las variables se vuelvan a cargar desde la memoria principal. Entonces podremos ver todas las escrituras que se hicieron visibles mediante la liberación anterior.

Las lecturas y escrituras en los campos son linealizables si el campo es volátil o si el campo está protegido por un bloqueo único que adquieren todos los lectores y escritores.

Bloqueos y bloqueos sincronizados

Un hilo puede lograr la exclusión mutua ya sea ingresando a un bloque o método sincronizado, que adquiere un bloqueo implícito, [14] [2] o adquiriendo un bloqueo explícito (como el ReentrantLockdel java.util.concurrent.lockspaquete [15] ). Ambos enfoques tienen las mismas implicaciones para el comportamiento de la memoria. Si todos los accesos a un campo en particular están protegidos por el mismo bloqueo, entonces las lecturas y escrituras en ese campo son linealizables (atómicas).

Campos volátiles

Cuando se aplica a un campo, la volatilepalabra clave Java garantiza que:

  1. (En todas las versiones de Java) Existe un orden global en las lecturas y escrituras de una volatilevariable. Esto implica que cada subproceso que acceda a un volatilecampo leerá su valor actual antes de continuar, en lugar de (potencialmente) usar un valor almacenado en caché. (Sin embargo, no hay garantía sobre el orden relativo de las lecturas y escrituras volátiles con las lecturas y escrituras regulares, lo que significa que generalmente no es una construcción de subprocesos útil).
  2. (En Java 5 o posterior) Las lecturas y escrituras volátiles establecen una relación de "sucede antes" , muy similar a la adquisición y liberación de un mutex. [16] Esta relación es simplemente una garantía de que las escrituras en memoria de una declaración específica sean visibles para otra declaración específica.

Los volatilecampos A son linealizables. Leer un volatilecampo es como adquirir un bloqueo: la memoria de trabajo se invalida y el volatilevalor actual del campo se vuelve a leer desde la memoria. Escribir un volatilecampo es como liberar un bloqueo: el volatilecampo se vuelve a escribir inmediatamente en la memoria.

Campos finales

Un campo declarado como finalno puede modificarse una vez que se ha inicializado. [17] Los campos de un objeto finalse inicializan en su constructor. Mientras la thisreferencia no se libere del constructor antes de que este regrese, el valor correcto de cualquier finalcampo será visible para otros subprocesos sin sincronización. [18]

Historia

Desde JDK 1.2 , Java ha incluido un conjunto estándar de clases de colección, el marco de colecciones de Java

Doug Lea , quien también participó en la implementación del marco de colecciones de Java, desarrolló un paquete de concurrencia , que comprende varias primitivas de concurrencia y una gran batería de clases relacionadas con colecciones. [19] Este trabajo continuó y se actualizó como parte de JSR 166, que fue presidido por Doug Lea.

JDK 5.0 incorporó muchas novedades y aclaraciones al modelo de concurrencia de Java. Las API de concurrencia desarrolladas por JSR 166 también se incluyeron como parte de JDK por primera vez. JSR 133 brindó soporte para operaciones atómicas bien definidas en un entorno multiprocesador/multiproceso.

Tanto la versión Java SE 6 como la Java SE 7 introdujeron versiones actualizadas de las API JSR 166, así como varias API nuevas adicionales.

Véase también

Notas

  1. ^ Goetz et al. 2006, págs. 15-17, §2 Seguridad de los hilos.
  2. ^ abcde Bloch 2018, pp. 126–129, Capítulo §11, Elemento 78: Sincronizar el acceso a datos mutables compartidos.
  3. ^ Goetz et al. 2006, págs. 125-126, §6.3.2 Tareas que generan resultados: invocables y futuras.
  4. ^ Goetz et al. 2006, págs. 95–98, §5.5.2 FutureTask.
  5. ^ Bloch 2018, pp. 336–337, Capítulo §11, ítem 84 No dependa del programador de hilos.
  6. ^ Bloch 2018, p. 311, Capítulo §11 Concurrencia.
  7. ^ Bloch 2018, págs. 323–324, Capítulo §5, elemento 80: Preferir ejecutores, tareas y flujos a subprocesos.
  8. ^ Goetz et al. 2006, págs. 138-141, §7.1.1 Interrupción.
  9. ^ Goetz et al. 2006, págs. 92–94, §5.4 Métodos bloqueantes e interrumpibles.
  10. ^ Oracle. «Interface Thread.UncaughtExceptionHandler» . Consultado el 10 de mayo de 2014 .
  11. ^ "Muerte silenciosa de un subproceso debido a excepciones no controladas". literatejava.com . 10 de mayo de 2014 . Consultado el 10 de mayo de 2014 .
  12. ^ Goetz et al. 2006, págs. 338–339, §16.1.1 Modelos de memoria de plataforma.
  13. ^ Herlihy, Maurice y Nir Shavit. "El arte de la programación multiprocesador". PODC. Vol. 6. 2006.
  14. ^ Goetz et al. 2006, págs. 25-26, §2.3.1 Cerraduras intrínsecas.
  15. ^ Goetz et al. 2006, págs. 277–278, §13 Bloqueos explícitos.
  16. ^ Sección 17.4.4: Orden de sincronización "Especificación del lenguaje Java®, Java SE 7 Edition". Oracle Corporation . 2013 . Consultado el 12 de mayo de 2013 .
  17. ^ Goetz et al. 2006, p. 48, §3.4.1 Campos finales.
  18. ^ Goetz et al. 2006, págs. 41–42, §3.2.1 Prácticas de construcción seguras.
  19. ^ Doug Lea . "Descripción general del paquete util.concurrent versión 1.3.4" . Consultado el 1 de enero de 2011. Nota: tras el lanzamiento de J2SE 5.0, este paquete entra en modo de mantenimiento: solo se publicarán las correcciones esenciales. El paquete java.util.concurrent de J2SE5 incluye versiones mejoradas, más eficientes y estandarizadas de los componentes principales de este paquete.

Referencias

Enlaces externos