Articulo de referencia

Concurrencia en Java

El lenguaje de programación Java y la máquina virtual Java (JVM) están diseñados para admitir la programación concurrente . Toda la ejecución se lleva a cabo en el contexto de h...

El lenguaje de programación Java y la máquina virtual Java (JVM) están diseñados para admitir la programación concurrente . Toda la ejecución se lleva a cabo en el contexto de hilos . Muchos hilos independientes pueden acceder a objetos y recursos, cada uno con su propia ruta de ejecución, pero con acceso a cualquier objeto del programa. El acceso de lectura y escritura a los objetos debe estar debidamente coordinado (o " sincronizado ") entre los hilos. [ 1 ] [ 2 ] La sincronización de hilos garantiza que los objetos sean modificados por un solo hilo a la vez y evita que los hilos accedan a objetos parcialmente actualizados durante la modificación por otro hilo. [ 2 ]

Procesos e hilos

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 centra principalmente en los hilos (también llamados procesos ligeros ). La ejecución de múltiples procesos solo es posible con varias máquinas virtuales Java.

objetos de hilo

Los hilos comparten los recursos del proceso, incluyendo la memoria y los archivos abiertos. Esto permite una comunicación eficiente, pero potencialmente problemática. [ 2 ] Cada aplicación tiene al menos un hilo llamado hilo principal. El hilo principal tiene la capacidad de crear hilos adicionales como Runnableobjetos Callable. La java.lang.Callableinterfaz es similar a java.lang.Runnableen el sentido de que ambas están diseñadas para clases cuyas instancias son potencialmente ejecutadas por otro hilo. [ 3 ] Sin embargo, un java.lang.Runnableno devuelve un resultado y no puede lanzar una excepción verificada . [ 4 ]

Cada hilo puede programarse [ 5 ] en un núcleo de CPU diferente [ 6 ] o utilizar la división de tiempo en un único procesador de hardware, o bien en varios procesadores de hardware. No existe una solución general sobre cómo se asignan los hilos de Java a los hilos nativos del sistema operativo. Cada implementación de la JVM puede hacerlo de manera diferente.

Cada hilo está asociado con una instancia de la clase java.lang.Thread. Los hilos se pueden gestionar utilizando directamente los java.lang.Threadobjetos o indirectamente mediante mecanismos abstractos como java.util.concurrent.Executoro clases de tareas. [ 7 ] Java proporciona las siguientes clases de tareas:

  • java.util.concurrent.ForkJoinTask<V>y sus descendientes:
    • java.util.concurrent.CountedCompleter<T>
    • java.util.concurrent.RecursiveAction(es ForkJoinTask<Void>)
    • java.util.concurrent.RecursiveTask<V>
  • java.util.concurrent.FutureTask<V>
    • javafx.concurrent.Task<V>(de JavaFX )

Como parte del Proyecto Loom en Java 21, se introdujeron los hilos virtuales , que permiten código síncrono, bloqueante y normal mientras escalan a millones de tareas. [ 8 ] [ 9 ] El hilo clásico de Java se llama "hilo de plataforma", que corresponde aproximadamente a un hilo del sistema operativo, pero los hilos virtuales de Java son ligeros y administrados por la JVM, programados en un pequeño grupo de hilos reales del sistema operativo. Los hilos virtuales siguen siendo java.lang.Thread, pero se crean con métodos de fábrica como ofVirtual(). Aunque el código sigue siendo síncrono y bloqueante, los hilos físicos del sistema operativo son asíncronos; al detenerse para esperar, la JVM desvincula el código del hilo físico, ejecutando otras tareas hasta que el sistema operativo envía una notificación de evento asíncrono para retomar el código donde lo dejó para terminar.

Inicio del hilo

Se puede iniciar un hilo proporcionando un java.lang.Runnableobjeto:

public class HelloRunnable implements Runnable { @Override public void run () { System . out . println ( "Hola desde el hilo!" ); }public static void main ( String [] args ) { Thread t = new Thread ( new HelloRunnable ()); t . start (); } }

O bien, mediante la creación de subclases:

public class HelloThread extends Thread { @Override public void run () { System . out . println ( "Hola desde el hilo!" ); }public static void main ( String [] args ) { HelloThread t = new HelloThread (); t . start (); } }

Interrumpe

Una interrupción le indica a un hilo que debe detener lo que está haciendo y hacer otra cosa. Un hilo envía una interrupción invocando interrupt()en un java.lang.Threadpara el hilo que se va a interrumpir. El mecanismo de interrupción se implementa utilizando una bandera interna que marca su estado de interrupción, que se establece al llamar a interrupt(). [ 10 ] [ 11 ] Cualquier método que salga lanzando una java.lang.InterruptedExceptionborra el estado de interrupción cuando lo hace, pero existe la posibilidad de que el estado de interrupción se establezca inmediatamente de nuevo, por otro hilo llamando a interrupt().

Históricamente, los hilos podían detenerse inmediatamente mediante este stop()método; al llamarlo, java.lang.ThreadDeathse lanzaba una excepción que limpiaba el hilo. Este método era peligroso, ya que podía dejar los datos en un estado corrupto, y finalmente se eliminó en Java 20. [ 12 ]

Se une

Este java.lang.Thread#join()método permite que una java.lang.Threadtarea espere a que otra finalice.

Excepciones

Las excepciones no controladas lanzadas por el código terminarán el hilo. El hilo principal imprime las excepciones en la consola, pero los hilos creados por el usuario necesitan un manejador registrado para hacerlo. [ 13 ] [ 14 ]

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 a menudo no se ejecuta en el orden en que fue escrito. El compilador , el procesador y el subsistema de memoria lo reordenan por razones de rendimiento. No hay garantía de linealización , ni siquiera de consistencia secuencial , [ 15 ] al leer o escribir campos de objetos compartidos. Esto permite optimizaciones del compilador (como la asignación de registros , la eliminación de subexpresiones comunes y la eliminación de lecturas redundantes ) mediante la reordenación de las operaciones de lectura y escritura en memoria. [ 16 ]

Sincronización

Los hilos se comunican principalmente compartiendo el acceso a los campos y a los objetos a los que hacen referencia dichos campos. Esta forma de comunicación es extremadamente eficiente, pero da lugar a dos tipos de errores: interferencia entre hilos y errores de consistencia de memoria.

Las reordenaciones pueden tener efecto en programas multihilo mal sincronizados , donde un hilo puede observar los efectos de otros hilos y detectar que los accesos a variables se vuelven visibles para otros hilos en un orden diferente al de ejecución o al especificado en el programa.

Cuando los hilos deben interactuar entre sí, se utiliza la sincronización. Para sincronizar hilos, Java utiliza monitores , un mecanismo de alto nivel que permite que solo un hilo a la vez ejecute una región de código protegida por el monitor. El comportamiento de los monitores se explica mediante bloqueos ; cada objeto tiene un bloqueo asociado.

La sincronización tiene muchas implementaciones. Cabe destacar la exclusión mutua , donde solo un hilo puede controlar un monitor a la vez; por lo tanto, sincronizarse en un monitor significa que una vez que un hilo entra en un bloque sincronizado protegido por un monitor, ningún otro hilo puede entrar en un bloque protegido por ese monitor hasta que el primer hilo salga del bloque sincronizado. [ 2 ]

La sincronización garantiza que las escrituras en memoria realizadas por un hilo antes o durante un bloque sincronizado sean visibles de forma predecible para otros hilos que se sincronizan en el mismo monitor. Una vez que synchronizedfinaliza un bloque, se libera el monitor, lo que vacía la caché a la memoria principal, de modo que las escrituras realizadas por este hilo sean visibles para los demás. Antes de que synchronizedcomience un bloque, se adquiere el monitor, invalidando la caché del procesador local para que las variables se recarguen desde la memoria principal. En ese momento, todas las escrituras realizadas por la liberación anterior son visibles.

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

Bloqueos y cerraduras sincronizadas

Un hilo puede lograr la exclusión mutua ya sea entrando en un bloque o método sincronizado, que adquiere un bloqueo implícito, [ 17 ] [ 2 ] o adquiriendo un bloqueo explícito (como el ReentrantLockdel java.util.concurrent.lockspaquete [ 18 ] ). 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).

La JVM también puede aplicar bloqueos en tiempo de ejecución: esto se conoce como la "técnica de bloqueo sesgado sin almacenamiento" o "bloqueo sesgado". [ 19 ]

Campos volátiles

Cuando se aplica a un campo, volatilela palabra clave garantiza un orden global para las lecturas y escrituras en una volatilevariable. Esto implica que cada hilo 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 respecto a las lecturas y escrituras regulares.

Desde Java 5, volatilelas lecturas y escrituras establecen una relación de precedencia , similar a la adquisición y liberación de un mutex. [ 20 ] Esta relación es simplemente una garantía de que las escrituras en memoria realizadas por una instrucción específica son visibles para otra instrucción específica.

volatileLos campos 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 de la memoria. Escribir en un volatilecampo es como liberar un bloqueo: el volatilecampo se escribe inmediatamente de nuevo en la memoria.

Campos finales

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

Atómica y cerraduras

Java ofrece características de atomicidad en java.util.concurrent.atomic. Estas proporcionan tipos sin bloqueo y seguros para subprocesos con operaciones atómicas, al tiempo que evitan synchronized.

Los tipos primitivos y los conjuntos de tipos primitivos tienen sus propios tipos atómicos (como java.util.concurrent.atomic.AtomicIntegery java.util.concurrent.atomic.AtomicIntegerArray), y para los tipos de referencia java.util.concurrent.atomic.AtomicReference<T>se proporciona la clase.

import java.util.concurrent.atomic.AtomicReference ;AtomicReference < String > ref = new AtomicReference <> ( "Hola..." ); ref . set ( "...mundo!" ); boolean success = ref . compareAndSet ( "...mundo!" , "Hola, mundo!" ); System . out . println ( ref . get ()); // "Hola, mundo!"

Futuros

En lugar de , se proporciona java.lang.Threadotra clase para computación asíncrona [ 23 ] ; esto puede verse como similar a un en C# , aunque Java carece de corrutinas explícitas como las de C#.java.util.concurrent.CompletableFuture<T>System.Threading.Tasks.Task<T>

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 un amplio conjunto de clases relacionadas con colecciones. [ 24 ] Este trabajo fue continuado y actualizado como parte de JSR 166, presidido por Doug Lea.

JDK 5.0 incorporó numerosas adiciones y aclaraciones al modelo de concurrencia de Java. Las API de concurrencia desarrolladas por JSR 166 también se incluyeron por primera vez en el JDK. JSR 133 proporcionó soporte para operaciones atómicas bien definidas en un entorno multihilo/multiprocesador.

Tanto las versiones Java SE 6 como 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 , pp. 15–17, §2 Seguridad de las roscas.
  2. 1 2 3 4 5 Bloch 2018 , págs. 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 dan resultados: Llamables y Futuras.
  4. Goetz et al. 2006 , pp. 95–98, §5.5.2 FutureTask.
  5. Bloch 2018 , págs. 336–337, Capítulo §11 Punto 84 No dependa del planificador de subprocesos.
  6. Bloch 2018 , pág. 311, Capítulo §11 Concurrencia.
  7. Bloch 2018 , págs. 323–324, Capítulo §5 Punto 80: Preferir ejecutores, tareas y flujos a hilos.
  8. "Hilos virtuales" . Centro de ayuda de Oracle . Consultado el 10 de septiembre de 2024 .
  9. "JEP 491: Sincronizar hilos virtuales sin fijación" . OpenJDK . Consultado el 30 de marzo de 2025 .
  10. Goetz et al. 2006 , pp. 138–141, §7.1.1 Interrupción.
  11. Goetz et al. 2006 , pp. 92–94, §5.4 Métodos de bloqueo e interrumpibles.
  12. OpenJDK (23 de septiembre de 2025). "Nota de la versión: Thread.stop se ha eliminado" . bugs.openjdk.org . OpenJDK.
  13. Oracle. "Interface Thread.UncaughtExceptionHandler" . Consultado el 10 de mayo de 2014 .
  14. "Muerte silenciosa de un hilo por excepciones no controladas" . literatejava.com . 10 de mayo de 2014. Consultado el 10 de mayo de 2014 .
  15. Goetz et al. 2006 , pp. 338–339, §16.1.1 Modelos de memoria de plataforma.
  16. Herlihy, Maurice y Nir Shavit. "El arte de la programación multiprocesador". PODC. Vol. 6. 2006.
  17. Goetz et al. 2006 , pp. 25–26, §2.3.1 Bloqueos intrínsecos.
  18. Goetz et al. 2006 , pp. 277–278, §13 Bloqueos explícitos.
  19. "Sincronización y bloqueo de objetos" .
  20. Sección 17.4.4: Orden de sincronización "Especificación del lenguaje Java®, edición Java SE 7" . Oracle Corporation . 2013. Consultado el 12 de mayo de 2013 .
  21. Goetz et al. 2006 , p. 48, §3.4.1 Campos finales.
  22. Goetz et al. 2006 , pp. 41–42, §3.2.1 Prácticas de construcción seguras.
  23. Oracle Corporation. "Clase CompletableFuture<T>" . docs.oracle.com . Oracle Corporation . Consultado el 27 de mayo de 2026 .
  24. 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 correcciones esenciales. El paquete java.util.concurrent de J2SE5 incluye versiones mejoradas, más eficientes y estandarizadas de los componentes principales de este paquete.

Bibliografía

  • Bloch, Joshua (2018). «Java eficaz: Guía del lenguaje de programación» (tercera  ed.). Addison-Wesley. ISBN 978-0134685991.
  • Goetz, Brian; Peierls, Tim; Bloch, Joshua; Bowbeer, Joseph; Holmes, David; Lea, Doug (2006). Java Concurrency in Practice . Addison Wesley. ISBN 0-321-34960-1.
  • Lea, Doug (1999). Programación concurrente en Java: principios y patrones de diseño . Addison Wesley. ISBN 0-201-31009-0.
  • Tutorial de concurrencia en Java de Oracle
  • Página del modelo de memoria de Java de William Pugh
  • Tutorial de concurrencia de Java por Jakob Jenkov
  • Animaciones de concurrencia en Java por Victor Grazi
  • Comprobador de seguridad de subprocesos para clases Java