Ideas clave
1. La seguridad en hilos consiste en gestionar el estado compartido y mutable
Escribir código seguro para hilos es, en esencia, gestionar el acceso al estado, y en particular al estado compartido y mutable.
Definiendo la seguridad en hilos. La seguridad en hilos significa que una clase se comporta correctamente cuando es accedida desde múltiples hilos, sin importar el orden o la intercalación, y sin que el llamador tenga que añadir sincronización adicional. Se trata de garantizar que los invariantes y las postcondiciones se mantengan incluso en un entorno concurrente. La clave es proteger los datos de accesos concurrentes no controlados.
Tres formas de arreglar programas defectuosos. Cuando varios hilos acceden a la misma variable mutable sin la sincronización adecuada, el programa está roto. Hay tres maneras de solucionarlo: (1) No compartir la variable de estado entre hilos; (2) Hacer la variable de estado inmutable; o (3) Usar sincronización cada vez que se acceda a la variable de estado.
La encapsulación es fundamental. Técnicas orientadas a objetos como la encapsulación y el ocultamiento de datos son cruciales para crear clases seguras para hilos. Cuanto menos código tenga acceso a una variable en particular, más fácil es asegurar que todo ese código use la sincronización adecuada y más sencillo es razonar sobre las condiciones bajo las cuales se puede acceder a dicha variable.
2. La sincronización garantiza atomicidad y visibilidad
Para preservar la consistencia del estado, actualiza las variables relacionadas en una única operación atómica.
Atomicidad y condiciones de carrera. La sincronización asegura que las operaciones se ejecuten de forma atómica, evitando condiciones de carrera donde la corrección de un cálculo depende del orden impredecible de múltiples hilos. Sin atomicidad, operaciones como incrementar un contador o inicializar perezosamente pueden producir resultados incorrectos.
Bloqueos intrínsecos para la atomicidad. Java ofrece bloqueos intrínsecos (usando la palabra clave synchronized) para garantizar la atomicidad. Solo un hilo puede ejecutar un bloque de código protegido por un mismo bloqueo a la vez. Esto asegura que acciones compuestas, como verificar y luego actuar o leer-modificar-escribir, se ejecuten como una unidad indivisible.
Bloqueo y visibilidad en memoria. La sincronización no solo implica exclusión mutua; también garantiza la visibilidad en memoria. Para asegurar que todos los hilos vean los valores más actualizados de variables compartidas y mutables, tanto los hilos lectores como los escritores deben sincronizarse con un mismo bloqueo. Esto evita datos obsoletos y asegura que los cambios hechos por un hilo sean visibles para los demás.
3. La publicación segura es esencial para compartir objetos
Los objetos inmutables pueden ser usados con seguridad por cualquier hilo sin sincronización adicional, incluso cuando no se usan mecanismos de sincronización para publicarlos.
¿Qué es la publicación segura? Publicar un objeto significa ponerlo a disposición de código fuera de su ámbito actual. La publicación segura garantiza que tanto la referencia al objeto como su estado sean visibles para otros hilos al mismo tiempo. Sin publicación segura, los hilos pueden ver datos obsoletos o inconsistentes.
Modos comunes de publicación segura:
- Inicializar una referencia a un objeto desde un inicializador estático
- Guardar una referencia en un campo
volatileo en unAtomicReference - Guardar una referencia en un campo
finalde un objeto correctamente construido - Guardar una referencia en un campo protegido adecuadamente por un bloqueo
Inmutabilidad y publicación segura. Los objetos inmutables pueden publicarse por cualquier mecanismo, incluso sin sincronización, porque su estado no puede modificarse tras la construcción, eliminando el riesgo de condiciones de carrera. Los objetos efectivamente inmutables, cuyo estado no cambiará tras la publicación, deben publicarse de forma segura. Los objetos mutables deben publicarse de forma segura y ser seguros para hilos o estar protegidos por un bloqueo.
4. Componer clases seguras para hilos para una concurrencia robusta
Para cada invariante que involucre más de una variable, todas las variables implicadas deben estar protegidas por el mismo bloqueo.
Diseñando clases seguras para hilos. Diseñar una clase segura para hilos implica identificar las variables de estado, definir los invariantes y establecer una política de sincronización. La encapsulación es vital para manejar la complejidad y asegurar que el estado se acceda con el bloqueo adecuado.
Confinamiento de instancia. El confinamiento de instancia consiste en encapsular el estado mutable dentro de un objeto y protegerlo del acceso concurrente sincronizando cualquier ruta de código que acceda al estado usando el bloqueo intrínseco del objeto. Esto simplifica el análisis de seguridad para hilos y permite estrategias flexibles de bloqueo.
Delegar la seguridad en hilos. La seguridad en hilos puede delegarse a objetos seguros para hilos, pero esto requiere considerar cuidadosamente los invariantes y dependencias de estado. Si una clase tiene acciones compuestas, debe proveer su propio bloqueo para garantizar atomicidad.
5. Aprovechar colecciones concurrentes para un rendimiento escalable
Reemplazar colecciones sincronizadas por colecciones concurrentes puede ofrecer mejoras dramáticas en escalabilidad con poco riesgo.
Limitaciones de las colecciones sincronizadas. Las colecciones sincronizadas, como Vector y Hashtable, logran seguridad para hilos serializando todo acceso al estado de la colección. Esto puede causar baja concurrencia y problemas de escalabilidad, especialmente bajo carga intensa.
Ventajas de las colecciones concurrentes. Las colecciones concurrentes, como ConcurrentHashMap y CopyOnWriteArrayList, están diseñadas para acceso concurrente desde múltiples hilos. Usan mecanismos de bloqueo más finos y algoritmos no bloqueantes para permitir mayor concurrencia y escalabilidad.
ConcurrentHashMap y CopyOnWriteArrayList. ConcurrentHashMap es un reemplazo concurrente para implementaciones sincronizadas basadas en mapas hash, mientras que CopyOnWriteArrayList es un reemplazo concurrente para listas sincronizadas en casos donde la operación dominante es la iteración. Estas clases proveen iteradores que no lanzan ConcurrentModificationException, eliminando la necesidad de bloquear la colección durante la iteración.
6. Usar colas bloqueantes para implementar el patrón productor-consumidor
Las colas acotadas son una herramienta poderosa para la gestión de recursos en aplicaciones confiables: hacen que tu programa sea más robusto frente a sobrecargas al limitar actividades que podrían generar más trabajo del que se puede manejar.
Patrón productor-consumidor. Las colas bloqueantes son ideales para implementar el patrón productor-consumidor, donde los productores colocan datos en la cola y los consumidores los extraen. Este patrón desacopla la identificación del trabajo de su ejecución, simplificando el desarrollo y la gestión de la carga.
Implementaciones de BlockingQueue. La biblioteca de clases incluye varias implementaciones de BlockingQueue, como LinkedBlockingQueue, ArrayBlockingQueue y PriorityBlockingQueue. SynchronousQueue es un tipo especial de cola bloqueante que no mantiene espacio de almacenamiento para elementos en cola, facilitando la transferencia directa entre productores y consumidores.
Confinamiento serial de hilos. Las colas bloqueantes facilitan el confinamiento serial de hilos para transferir la propiedad de objetos de productores a consumidores. Esto permite que objetos mutables se transfieran de forma segura entre hilos sin sincronización adicional.
7. La cancelación y el apagado requieren mecanismos cooperativos
La interrupción suele ser la forma más sensata de implementar la cancelación.
Cancelación cooperativa. Java no ofrece un mecanismo para forzar de forma segura que un hilo se detenga. En cambio, proporciona la interrupción, un mecanismo cooperativo que permite a un hilo pedir a otro que deje lo que está haciendo.
Políticas de interrupción. Los hilos deben tener una política de interrupción que determine cómo responden a las solicitudes de interrupción. La política más sensata es alguna forma de cancelación a nivel de hilo o servicio: salir tan rápido como sea posible, limpiando si es necesario y posiblemente notificando a alguna entidad propietaria que el hilo está terminando.
Apagado de ExecutorService. ExecutorService ofrece métodos para gestionar el ciclo de vida, incluyendo shutdown (apagado ordenado) y shutdownNow (apagado abrupto). Estos métodos permiten a las aplicaciones terminar grupos de hilos y otros servicios de manera controlada.
8. La ley de Amdahl limita la escalabilidad; reduce la serialización
La principal amenaza para la escalabilidad en aplicaciones concurrentes es el bloqueo exclusivo de recursos.
Ley de Amdahl. La ley de Amdahl describe cuánto puede acelerarse teóricamente un programa con recursos computacionales adicionales, basándose en la proporción de componentes paralelizables y seriales. Destaca la importancia de reducir la serialización para mejorar la escalabilidad.
Reducir la contención de bloqueos. Hay tres formas de reducir la contención de bloqueos: (1) Reducir la duración durante la cual se mantienen los bloqueos; (2) Reducir la frecuencia con la que se solicitan los bloqueos; o (3) Reemplazar bloqueos exclusivos por mecanismos de coordinación que permitan mayor concurrencia.
División y segmentación de bloqueos. La división de bloqueos implica usar bloqueos separados para proteger múltiples variables de estado independientes que antes estaban protegidas por un solo bloqueo. La segmentación de bloqueos extiende este concepto a un conjunto variable de objetos, usando múltiples bloqueos para proteger diferentes subconjuntos.
9. Comprender los costos que introducen los hilos
Asignar objetos suele ser más barato que sincronizar.
Sobrecarga por cambio de contexto. Usar múltiples hilos siempre introduce ciertos costos de rendimiento comparado con un enfoque de un solo hilo. Estos incluyen la sobrecarga asociada a la coordinación entre hilos (bloqueos, señales y sincronización de memoria), aumento en cambios de contexto, creación y destrucción de hilos, y sobrecarga de planificación.
Costos de sincronización de memoria. La sincronización genera tráfico en el bus de memoria compartida, que tiene ancho de banda limitado y es compartido por todos los procesadores. Esto puede inhibir optimizaciones del compilador e introducir costos adicionales de rendimiento.
Bloqueo y capacidad de respuesta. Cuando hay contención por bloqueo, los hilos que pierden deben bloquearse. La JVM puede implementar el bloqueo mediante espera activa (spin-waiting) o suspendiendo el hilo bloqueado a través del sistema operativo. Ambos enfoques tienen costos de rendimiento.
10. Probar programas concurrentes requiere estrategias específicas
El objetivo de las pruebas no es tanto encontrar errores como aumentar la confianza en que el código funciona como se espera.
Desafíos de las pruebas concurrentes. Los programas concurrentes tienen un grado de no determinismo que los secuenciales no tienen, aumentando el número de interacciones y modos de fallo potenciales que deben planificarse y analizarse. Las fallas potenciales pueden ser ocurrencias probabilísticas raras en lugar de deterministas.
Pruebas de corrección. Las pruebas de seguridad verifican que el comportamiento de una clase se ajuste a su especificación, usualmente en forma de pruebas de invariantes. Las pruebas de vivacidad aseguran que "algo bueno eventualmente sucede", incluyendo pruebas de progreso y no progreso.
Pruebas de rendimiento. Las pruebas de rendimiento miden métricas de rendimiento de extremo a extremo para casos de uso representativos, como rendimiento, capacidad de respuesta y escalabilidad. Estas pruebas deben ejecutarse bajo condiciones realistas y con carga suficiente para exponer posibles cuellos de botella.
11. Los bloqueos explícitos ofrecen control avanzado sobre la sincronización
ReentrantLock es una herramienta avanzada para situaciones donde el bloqueo intrínseco no es práctico. Úsalo si necesitas sus características avanzadas: adquisición de bloqueo con tiempo límite, sondeo o interrupción, cola justa o bloqueo no estructurado. De lo contrario, prefiere
synchronized.
Interfaz Lock. La interfaz Lock define operaciones abstractas de bloqueo, ofreciendo opciones de adquisición incondicional, sondeada, con tiempo límite e interrumpible. A diferencia del bloqueo intrínseco, todas las operaciones de bloqueo y desbloqueo son explícitas.
ReentrantLock. ReentrantLock implementa Lock, proporcionando las mismas garantías de exclusión mutua y visibilidad de memoria que synchronized. También ofrece semánticas de bloqueo reentrante y soporta todos los modos de adquisición definidos por Lock.
ReadWriteLock. ReadWriteLock expone dos objetos Lock: uno para lectura y otro para escritura. Esto permite múltiples lectores simultáneos pero solo un escritor, mejorando la concurrencia para estructuras de datos mayormente de lectura.
12. AbstractQueuedSynchronizer (AQS) simplifica el desarrollo de sincronizadores
Un sincronizador es cualquier objeto que coordina el flujo de control de hilos basado en su estado.
Marco AQS. AQS es un marco para construir bloqueos y sincronizadores, proporcionando una clase base común para muchos sincronizadores en java.util.concurrent. Maneja muchos detalles de la implementación, como la cola FIFO de hilos en espera.
Operaciones de AQS. Las operaciones básicas que realiza un sincronizador basado en AQS son variantes de adquirir y liberar. La adquisición depende del estado y puede bloquear. La liberación no bloquea; puede permitir que hilos bloqueados en adquisición continúen.
AQS en la práctica. Muchas clases bloqueantes en java.util.concurrent, como ReentrantLock, Semaphore, ReentrantReadWriteLock, CountDownLatch, SynchronousQueue y FutureTask, están construidas usando AQS.
Resumen de reseñas
Java Concurrency in Practice es ampliamente reconocido como una lectura imprescindible para los desarrolladores de Java. Los críticos destacan su cobertura exhaustiva de los conceptos de concurrencia, desde los temas más básicos hasta los más avanzados. El libro es elogiado por sus explicaciones claras, ejemplos prácticos y la progresiva construcción del conocimiento. Muchos lectores valoran especialmente sus aportes sobre el modelo de memoria de Java y las APIs relacionadas con la concurrencia. Aunque algunos mencionan que está un poco desactualizado, los principios fundamentales siguen siendo vigentes. Los lectores subrayan su importancia para comprender e implementar código concurrente seguro y eficiente, considerándolo una obra esencial para los programadores de Java.
Preguntas frecuentes
What's Java Concurrency in Practice about?
- Focus on Concurrency: Java Concurrency in Practice by Brian Goetz is a comprehensive guide to writing concurrent applications in Java, addressing the complexities and challenges of multithreading.
- Practical Techniques: It covers fundamental concepts like thread safety, synchronization, and the Java Memory Model, providing practical techniques for developing, testing, and debugging multithreaded programs.
- Target Audience: The book is suitable for Java developers of all levels, offering insights into concurrency design patterns and real-world examples to enhance understanding.
Why should I read Java Concurrency in Practice?
- Essential for Java Developers: Understanding concurrency is crucial for building responsive and efficient Java applications, making this book a must-read for developers.
- Expert Insights: Written by Brian Goetz, a leading expert in Java concurrency, the book offers deep and practical insights into the design and implementation of concurrency features.
- Real-World Applications: It includes real-world scenarios and solutions, helping developers apply concepts directly to their projects and improve code quality.
What are the key takeaways of Java Concurrency in Practice?
- Thread Safety Principles: Emphasizes managing access to shared, mutable state through synchronization and locks to ensure thread safety.
- Design Patterns: Introduces design patterns like the Java monitor pattern and thread confinement to structure concurrent applications effectively.
- Performance Considerations: Discusses performance optimization techniques, including minimizing lock contention and understanding Amdahl's Law for scalability.
What is thread safety according to Java Concurrency in Practice?
- Definition of Thread Safety: A class is thread-safe if it behaves correctly when accessed by multiple threads concurrently, maintaining consistent and valid state.
- Importance of Synchronization: Achieving thread safety requires synchronization mechanisms to coordinate access to shared mutable state.
- Invariants and Postconditions: Thread-safe classes must maintain their invariants and postconditions, ensuring state validity regardless of thread access.
How does Java Concurrency in Practice define the Java Memory Model?
- Purpose of the Memory Model: Defines how threads interact through memory, providing guarantees about visibility and ordering of operations in concurrent programming.
- Visibility Guarantees: Without synchronization, changes made by one thread may not be visible to others, emphasizing the need for proper synchronization.
- Atomicity and Ordering: Discusses how the memory model allows optimizations, affecting atomicity and ordering, and the importance of understanding these implications.
What is the Executor framework in Java Concurrency in Practice?
- Task Management Simplified: The Executor framework decouples task submission from execution policy, allowing flexible management of concurrent tasks.
- Thread Pooling: Supports thread pooling, improving resource management by reusing threads and reducing overhead.
- Simplified Concurrency: By using the Executor framework, developers can avoid complexities of thread management, focusing on task definition and execution policies.
What are some common concurrency hazards discussed in Java Concurrency in Practice?
- Race Conditions: Occur when the correctness of a computation depends on the relative timing of threads, leading to unexpected behavior.
- Deadlocks: Happen when threads are blocked forever, each waiting on the other to release a resource, with strategies provided to avoid them.
- Visibility Issues: Arise when one thread modifies a variable, but another thread does not see the updated value, stressing the need for proper synchronization.
How can I avoid deadlocks according to Java Concurrency in Practice?
- Consistent Lock Ordering: Acquire locks in a consistent global order to prevent cyclic dependencies between threads.
- Use Open Calls: Avoid calling alien methods while holding locks to prevent unexpected dependencies and potential deadlocks.
- Timed Lock Attempts: Implement timed lock attempts using explicit locks to regain control if a lock cannot be acquired within a reasonable timeframe.
What are the main synchronization techniques discussed in Java Concurrency in Practice?
- Intrinsic Locks: Simplest form of synchronization in Java, providing mutual exclusion for critical sections but can lead to contention.
- Explicit Locks:
ReentrantLockoffers more flexibility than intrinsic locks, allowing timed and interruptible lock acquisition. - Condition Variables: Allow threads to wait for certain conditions, crucial for implementing patterns like producer-consumer.
What is the significance of Amdahl's Law in Java Concurrency in Practice?
- Performance Limitation: Illustrates that the maximum speedup of a program is limited by the serial portion of the workload.
- Scalability Insight: Helps developers understand the impact of serial execution on overall application performance and scalability.
- Guides Optimization Efforts: Identifying serial components helps focus optimization efforts to improve scalability.
What are the best quotes from Java Concurrency in Practice and what do they mean?
- "The cost of fairness results primarily from blocking threads.": Highlights that fair algorithms can lead to performance issues due to increased blocking, requiring a balance between fairness and throughput.
- "Testing concurrent programs for correctness can be extremely challenging.": Reflects the difficulties in ensuring concurrent applications behave as expected, emphasizing rigorous testing.
- "The goal of testing is not so much to find errors as it is to increase confidence that the code works as expected.": Encourages a focus on building reliable systems rather than merely identifying bugs.
What are some advanced topics covered in Java Concurrency in Practice?
- Explicit Locks and Condition Variables: Advanced locking mechanisms like
ReentrantLockprovide more control over synchronization. - Nonblocking Algorithms: Use atomic operations instead of locks, offering performance and scalability advantages.
- The Java Memory Model: In-depth look at how it governs visibility and ordering of operations, essential for writing correct multithreaded applications.