22. Concurrencia y subprocesos en Java
La concurrencia es un concepto fundamental en la programación moderna, especialmente en Java. Con el aumento del número de procesadores y núcleos en los sistemas informáticos, se vuelve cada vez más importante saber cómo realizar tareas simultáneamente para aprovechar al máximo los recursos de hardware disponibles. En este capítulo, exploraremos los conceptos de concurrencia y subprocesos en Java, desde los aspectos básicos hasta los avanzados.
Fundamentos del hilo
En Java, un subproceso es la unidad de ejecución más pequeña que puede programar el sistema operativo. Una aplicación Java puede tener varios subprocesos ejecutándose simultáneamente, cada uno de los cuales maneja una parte diferente del trabajo. Esto permite que los programas realicen múltiples tareas, como responder a eventos del usuario mientras realizan cálculos en segundo plano.
Para crear un hilo en Java, hay dos formas principales:
- Extender la clase Thread: Cree una clase que extienda
java.lang.Thread
y anule el métodorun()
. Después de crear una instancia de su clase, llame al métodostart()
para ejecutar el hilo. - Implementación de la interfaz Runnable: cree una clase que implemente la interfaz
Runnable
e implemente el métodorun()
. Puede pasar una instancia de esta clase al constructorThread
y luego llamar astart()
.
clase MyThread extiende Thread { ejecución pública vacía() { // Código a ejecutar en un nuevo hilo } } clase MyRunnable implementa Runnable { ejecución pública vacía() { // Código a ejecutar en un nuevo hilo } } clase pública EjemploThread { público estático vacío principal (String [] argumentos) { MiHilo mt = nuevo MiHilo(); mt.start(); Hilo t = nuevo hilo (nuevo MyRunnable()); t.start(); } }
Gestión de subprocesos
La gestión manual de subprocesos puede ser compleja y propensa a errores. Java proporciona un conjunto de herramientas para ayudar a gestionar la ejecución de subprocesos, incluidos métodos para:
- Sincronización: Palabra clave
sincronizada
y clases en el paquetejava.util.concurrent
, comoBloqueos
,Semaphores
yCountDownLatch
ayudan a administrar el acceso a recursos compartidos. - Comunicación entre subprocesos: métodos como
wait()
,notify()
ynotifyAll()
son utilizado para la comunicación entre hilos. - Gestión de estados: métodos como
interrupt()
,join()
yisAlive()
ayudan a gestionar el estado y el ciclo de vida de los subprocesos.
Problemas de competencia
El desarrollo de aplicaciones competitivas plantea desafíos únicos, como:
- Interbloqueo: ocurre cuando dos o más subprocesos esperan indefinidamente entre sí para liberar recursos, lo que resulta en un interbloqueo.
- Inanición: ocurre cuando uno o más subprocesos no pueden avanzar debido a la monopolización de recursos por parte de otros subprocesos.
- Condiciones de carrera: Surgen cuando dos o más subprocesos acceden a un recurso compartido simultáneamente y el resultado final depende del orden de ejecución de los subprocesos.
Para evitar estos problemas, es fundamental comprender y aplicar técnicas adecuadas de sincronización y bloqueo.
Ejecutores y Servicios de Ejecutor
A partir de Java 5, la API de concurrencia introdujo el marco de ejecución, que simplifica la ejecución y gestión de subprocesos. Los ejecutores abstraen la creación y gestión de subprocesos y proporcionan un servicio de ejecutor que se puede utilizar para ejecutar tareas de forma asincrónica.
Ejemplos de ejecutores incluyen:
- ThreadPoolExecutor: Gestiona un grupo de subprocesos reutilizables.
- ScheduledThreadPoolExecutor: Permite la ejecución de tareas con retraso o periódicamente.
- Executors.newCachedThreadPool: crea un grupo de subprocesos que crea nuevos subprocesos según sea necesario, pero reutilizará subprocesos construidos previamente cuando estén disponibles.
ExecutorService ejecutor = Executors.newFixedThreadPool(4); ejecutor.execute(nuevo ejecutable() { ejecución pública vacía() { // Tarea a ejecutar } }); ejecutor.shutdown();
Competencia de alto nivel
Además de los ejecutores, la API de concurrencia de Java ofrece abstracciones de alto nivel como:
Future
yCallable
para trabajar con resultados de tareas asincrónicas.CompletionService
para gestionar un conjunto de tareas asincrónicas.Colecciones simultáneas
comoConcurrentHashMap
yBlockingQueue
para trabajar con colecciones en entornos concurrentes.
Buenas Prácticas de Competencia
Para escribir programas concurrentes robustos y eficientes en Java, es importante seguir algunas buenas prácticas:
- Minimiza el alcance de las secciones críticas utilizando la sincronización solo cuando sea estrictamente necesario.
- Evite interbloqueos de larga duración para reducir el riesgo de interbloqueo y mejorar la capacidad de respuesta de la aplicación.
- Prefiera las abstracciones de alto nivel de la API de concurrencia de Java a la gestión manual de subprocesos y sincronización.
- Utilice colecciones simultáneas para gestionar el acceso a los datos compartidos entre subprocesos.
- Esté consciente de los problemas comunes de simultaneidad, como condiciones de carrera, estancamientos e inanición, y sepa cómo evitarlos.
En resumen, la concurrencia en Java es una característica poderosa que, cuando se usa correctamente, puede generar aplicaciones más eficientes y con mayor capacidad de respuesta. Sin embargo, también introduce complejidad y posibles dificultades. Con una sólida comprensión de los conceptos y herramientas proporcionadas por la API de concurrencia de Java, los desarrolladores pueden crear programas concurrentes seguros y eficaces.