Introducción
¡Bienvenidos de vuelta a nuestra serie de concurrencia en Java! En los artículos anteriores, exploramos cómo se heredan los hilos y cómo crear y gestionar subprocesos de manera efectiva. Hoy, continuaremos nuestra inmersión en el mundo de la concurrencia, centrándonos en un aspecto crucial: la sincronización. Prepara tu brújula, ya que navegaremos a través de las olas de los problemas de concurrencia y cómo evitarlos.
La Importancia de la Sincronización
Cuando varios subprocesos comparten recursos o datos, es esencial sincronizarlos adecuadamente para evitar condiciones de carrera y resultados inesperados. La sincronización en Java se puede lograr utilizando palabras clave como synchronized o mediante objetos de bloqueo explícitos.
Uso de synchronized para Sincronización
La palabra clave synchronized puede aplicarse a métodos o bloques de código. Esto asegura que solo un subproceso pueda ejecutar el bloque sincronizado a la vez, evitando problemas de acceso concurrente a recursos compartido.
Ejemplo de Uso de synchronized
Supongamos que tenemos una clase llamada BankAccount que representa una cuenta bancaria con un saldo y queremos asegurarnos de que múltiples hilos no puedan acceder al saldo al mismo tiempo para evitar problemas de concurrencia.
En este ejemplo, los métodos deposit, withdraw y getBalance están marcados como synchronized. Esto significa que cuando un hilo está ejecutando cualquiera de estos métodos, adquiere un bloqueo en el objeto de la instancia de BankAccount, lo que evita que otros hilos accedan a los métodos sincronizados simultáneamente.
Ahora podemos crear múltiples hilos que interactúen con una instancia de BankAccount y garantizar que los métodos se ejecuten de manera segura y en orden:
En este ejemplo, los dos hilos interactúan con la misma instancia de BankAccount, pero debido al uso de synchronized, los métodos son ejecutados de manera segura y evitan posibles problemas de concurrencia, como condiciones de carrera y lecturas/escrituras incorrectas.
En Java, el método join() es utilizado para esperar a que un hilo termine su ejecución antes de que el hilo actual (llamado hilo principal) continúe ejecutándose. Cuando se llama al método join() en un hilo, el hilo actual se bloquea y espera hasta que el hilo en el que se llamó al método join() complete su ejecución.
En otras palabras, las líneas thread1.join() y thread2.join() aseguran que el hilo principal no continúe ejecutándose y llegue a la línea System.out.println("Final balance: " + account.getBalance()) hasta que ambos hilos thread1 y thread2 hayan terminado de ejecutarse por completo. Esto garantiza que obtengamos el saldo final de la cuenta bancaria después de que ambos hilos hayan realizado todas sus operaciones de depósito y retiro
Objetos de Bloqueo Explícitos
Además de synchronized, también se pueden usar objetos de bloqueo explícitos para lograr sincronización. Esto brinda un mayor control y flexibilidad sobre la sincronización.
Ejemplo de Uso de Objeto de Bloqueo
Utilizaremos el mismo ejemplo del banco, realizando algunas modificaciones a la clase, ahora utilizando locks para asegurar que las operaciones sean ejecutadas de manera sincronizada:
Ahora, los hilos de depósito y retiro son creados y lanzados en paralelo, pero las operaciones en la cuenta bancaria están protegidas por los locks, garantizando la coherencia y la integridad de los datos compartidos.
En el contexto del ejemplo de la cuenta bancaria, las secciones críticas de código son los métodos deposit y withdraw, ya que involucran cambios en el saldo de la cuenta. Al llamar a balanceLock.lock() antes de realizar operaciones en el saldo, se asegura que estas operaciones sean atómicas y se evita que múltiples hilos accedan simultáneamente y posiblemente alteren el saldo de manera inconsistente.
Es importante destacar que después de que un hilo haya terminado su trabajo en la sección crítica protegida por el bloqueo, debe liberar el bloqueo llamando a balanceLock.unlock() para permitir que otros hilos puedan adquirirlo y acceder a la sección crítica de manera segura. Si no se libera el bloqueo adecuadamente, podría resultar en bloqueos permanentes o problemas de concurrencia
Evitando Condiciones de Carrera
Una condición de carrera ocurre cuando varios subprocesos acceden y modifican un recurso compartido al mismo tiempo, lo que puede conducir a resultados inesperados. La sincronización adecuada evita estas situaciones problemáticas.
Beneficios de la Sincronización
- Consistencia de Datos: Garantiza que los datos sean consistentes y válidos en entornos concurrentes.
- Evita Condiciones de Carrera: Prevención de problemas causados por accesos concurrentes a recursos compartidos.
- Protege Recursos Críticos: Asegura que solo un subproceso pueda acceder a recursos críticos a la vez.
Desventajas de la Sincronización
- Bloqueo Excesivo: Demasiada sincronización puede resultar en bloqueos innecesarios y reducción del rendimiento.
- Posibilidad de Deadlocks: Una mala sincronización puede llevar a situaciones de bloqueo mutuo entre subproceso.
Conclusiones y Continuación
La sincronización es un pilar fundamental en la concurrencia en Java. En este artículo, hemos explorado cómo aplicar synchronized y objetos de bloqueo para evitar condiciones de carrera y garantizar la consistencia de los datos. En el próximo artículo, continuaremos nuestro viaje, explorando cómo manejar la comunicación y la coordinación entre subprocesos.
¡Mantente atento para seguir aprendiendo sobre cómo enfrentar los desafíos de la concurrencia en Java!
Top comments (0)