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 subprocesos. Se puede acceder a los objetos y recursos mediante muchos subprocesos independientes. Cada subproceso tiene su propia ruta de ejecución, pero potencialmente puede acceder a cualquier objeto del programa. El programador debe asegurarse de que el acceso de lectura y escritura a los objetos esté correctamente coordinado (o "sincronizado") entre subprocesos. La sincronización de subprocesos garantiza 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 parte de otro subproceso. El lenguaje Java tiene construcciones integradas para admitir esta coordinación.
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 ocupa principalmente de los subprocesos (también llamados procesos ligeros). Solo se pueden realizar múltiples procesos con varias JVM.
Objetos de pan
Los subprocesos comparten los recursos del proceso, incluida la memoria y los archivos abiertos. Esto permite una comunicación eficiente, pero potencialmente problemática. Cada aplicación tiene al menos un subproceso llamado subproceso principal. El subproceso principal tiene la capacidad de crear subprocesos adicionales como objetos Runnable
o Callable
. La interfaz Callable
es similar a Runnable
en el sentido de que ambas están diseñadas para clases cuyas instancias son ejecutadas potencialmente por otro subproceso. Sin embargo, un Runnable
no devuelve un resultado y no puede generar una excepción comprobada.
Cada subproceso se puede programar en un núcleo de CPU diferente o utilizar la segmentación de tiempo en un único procesador de hardware o en varios procesadores de hardware. No existe una solución general para la asignación de subprocesos de Java a subprocesos nativos del sistema operativo. Cada implementación de JVM puede hacerlo de forma diferente.
Cada subproceso está asociado a una instancia de la clase Thread
. Los subprocesos se pueden gestionar directamente mediante el uso de los objetos Thread
o indirectamente mediante el uso de mecanismos abstractos como los Executor
o las Task
.
Empezando un thread
Dos formas de iniciar un hilo:
Proporcionar un objeto ejecutable
público clase HolaRunnable implementos Runnable {} @Override público vacío Corre() {} Sistema.Fuera..println()"¡Hola por el hilo!"); } público estática vacío principal()String[] args) {} ()nuevo Thread()nuevo HolaRunnable())).Empieza(); } }
Subclase hilo
público clase Hola. extensivos Thread {} @Override público vacío Corre() {} Sistema.Fuera..println()"¡Hola por el hilo!"); } público estática vacío principal()String[] args) {} ()nuevo Hola.()).Empieza(); } }
Interrupciones
Una interrupción le indica a un subproceso que debe detener lo que está haciendo y hacer otra cosa. Un subproceso envía una interrupción invocando interrupt()
en el objeto Thread
para que el subproceso se interrumpa. El mecanismo de interrupción se implementa utilizando un indicador booleano interno conocido como el "estado interrumpido". Invocar interrupt()
activa este indicador. Por convención, cualquier método que finalice lanzando una InterruptedException
borra el estado interrumpido cuando lo hace. Sin embargo, siempre es posible que el estado interrumpido se vuelva a establecer inmediatamente si otro subproceso invoca interrupt()
.
Joins
El método
java.lang.Thread#join()
permite que un Thread
espere a que se complete 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.
Modelo de memoria
El modelo de memoria de Java describe cómo interactúan los subprocesos 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. El compilador, el procesador y el subsistema de memoria lo reordenan para lograr el máximo rendimiento. El lenguaje de programación Java no garantiza la linealización, o incluso la consistencia secuencial, 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.
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 hace posibles 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 ser capaz de 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 está haciendo 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.
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 está protegido por un bloqueo único que adquieren todos los lectores y escritores.
Cerraduras y bloques sincronizados
Un subproceso puede lograr la exclusión mutua ya sea ingresando a un bloque o método sincronizado, que adquiere un bloqueo implícito, o adquiriendo un bloqueo explícito (como el ReentrantLock
del paquete java.util.concurrent.locks
). 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 palabra clave Java volatile
garantiza que:
- (En todas las versiones de Java) Hay un orden global en las lecturas y escribe a
volatile
variable. Esto implica que cada hilo accediendo avolatile
campo leerá su valor actual antes de continuar, en lugar de (potencialmente) utilizando un valor caché. (Sin embargo, no hay garantía sobre el ordenamiento relativo de lecturas volátiles y escribe con lecturas y escritos regulares, lo que significa que generalmente no es una construcción de rosca útil.) - (En Java 5 o más tarde) Volátiles lee y escribe establecer una relación anterior a suceso, mucho como adquirir y liberar un mutex. Esta relación es simplemente una garantía de que la memoria escribe por una declaración específica son visibles a otra declaración específica.
Los campos volátiles son linealizables. Leer un campo volátil es como adquirir un bloqueo: la memoria de trabajo se invalida y el valor actual del campo volátil se vuelve a leer desde la memoria. Escribir un campo volátil es como liberar un bloqueo: el campo volátil se vuelve a escribir inmediatamente en la memoria.
Campos finales
Un campo declarado como final
no se puede modificar una vez que se ha inicializado. Los campos final
de un objeto se inicializan en su constructor. Mientras la referencia this
no se libere del constructor antes de que este regrese, el valor correcto de cualquier campo final
será visible para otros subprocesos sin sincronización.
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, que también participó en la implementación del marco de trabajo de colecciones de Java, desarrolló un paquete de concurrencia que comprende varias primitivas de concurrencia y una gran batería de clases relacionadas con las colecciones. Este trabajo se continuó y actualizó como parte de JSR 166, presidido por Doug Lea.
JDK 5.0 incorporó muchas adiciones y aclaraciones al modelo de concurrencia de Java. Las API de concurrencia desarrolladas por JSR 166 también se incluyeron como parte del JDK por primera vez. JSR 133 brindó soporte para operaciones atómicas bien definidas en un entorno multiproceso/multiprocesador.
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
- Concurrencia (ciencia informática)
- Patrón de moneda
- Fork-join model
- Barrera de memoria
- Modelos de memoria
- Thread safety
- ThreadSafe
- Java ConcurrentMap
Notas
- ^ Goetz et al. 2006, pp. 15–17, §2 Thread Safety.
- ^ Goetz et al. 2006, pp. 125–126, §6.3.2 Tareas de obtención de resultados: Callable y Future.
- ^ Goetz et al. 2006, pp. 95–98, §5.5.2 FutureTask.
- ^ Bloch 2018, págs. 336 a 337, Capítulo §11 Tema 84 No dependa del programador de hilos.
- ^ Bloch 2018, p. 311, Capítulo §11 Concurrencia.
- ^ Bloch 2018, págs. 323 a 324, Capítulo §5 Tema 80: Preferir a los ejecutores, tareas y flujos a los hilos.
- ^ Goetz et al. 2006, pp. 138–141, §7.1 Interruption.
- ^ Goetz et al. 2006, pp. 92–94, §5.4 Bloqueo y métodos interrumpibles.
- ^ Oracle. "Pases de Interfaz.. Retrieved 10 de mayo 2014.
- ^ "Silent Thread death from unhandled exceptions". literatejava.com10 de mayo de 2014. Retrieved 10 de mayo 2014.
- ^ Goetz et al. 2006, pp. 338–339, §16.1 Modelos de memoria de plataforma.
- ^ Herlihy, Maurice y Nir Shavit. "El arte de la programación multiprocesador". PODC. Vol. 6. 2006.
- ^ Goetz et al. 2006, pp. 25–26, §2.3.1 Cerraduras intrínsecas.
- ^ Goetz et al. 2006, pp. 277–278, §13 Explicit Locks.
- ^ Sección 17.4.4: Orden de sincronización "La especificación del lenguaje Java®, Java SE 7 Edition". Oracle Corporation. 2013. Retrieved 2013-05-12.
- ^ Goetz et al. 2006, p. 48, §3.4.1 Campos finales.
- ^ Goetz et al. 2006, pp. 41–42, §3.2.1 Prácticas de construcción seguras.
- ^ Doug Lea. "Overview of package util.concurrent Release 1.3.4". Retrieved 2011-01-01.
Nota: Al soltar J2SE 5.0, este paquete entra en modo de mantenimiento: Sólo se publicarán correcciones esenciales. J2SE5 paquete java.util.concurrent incluye versiones mejoradas, más eficientes y estandarizadas de los componentes principales en este paquete.
Referencias
- Bloch, Joshua (2018). "Effective Java: Programming Language Guide" (tercera edición). 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 simultánea en Java: Principios de diseño y patrones. Addison Wesley. ISBN 0-201-31009-0.
Enlaces externos
- Oracle Java concurrency tutorial
- página del modelo de memoria Java de William Pugh
- Java Concurrency Tutorial de Jakob Jenkov
- Java Concurrency Animations by Victor Grazi
- Controlador de seguridad para clases de Java