Control De Concurrencia Sincronización

Control de concurrencia: Sincronización

La concurrencia es el manejo simultáneo de múltiples hilos en un programa. En Java, esto se logra a través de las clases e interfaces del paquete java.util.concurrent, pero también mediante mecanismos básicos como synchronized y bloqueos.

Tema Descripción Código Ejemplo
Método sincronizado Controla acceso a métodos completos. public synchronized void metodo() {...}
Bloque sincronizado Controla acceso a fragmentos de código. synchronized (objeto) {...}
Sincronización estática Sincronización a nivel de clase (para métodos estáticos). public static synchronized void metodo()
ReentrantLock Proporciona bloqueo explícito y más flexible. lock.lock(); try {...} finally {lock.unlock();}
Objeto de bloqueo personalizado Permite sincronizar usando objetos arbitrarios. synchronized (lock) {...}
Problemas comunes Interbloqueos, condiciones de carrera, espera activa. Evitar prácticas problemáticas.

¿Qué es la sincronización?

La sincronización es una técnica para controlar el acceso a recursos compartidos (como objetos o datos) cuando varios hilos intentan acceder a ellos simultáneamente. Sin sincronización, los datos compartidos pueden quedar en un estado inconsistente.

Problema sin sincronización:

Supongamos que dos hilos intentan incrementar el mismo contador en una aplicación. Sin un mecanismo de control, es posible que ambos lean el mismo valor inicial, lo incrementen y guarden el mismo resultado, perdiendo uno de los incrementos.

¿Cómo se sincroniza en Java?

Java ofrece varios métodos para lograr sincronización:


1. Palabra clave synchronized

La palabra clave synchronized se puede usar en dos formas principales:

a) Bloqueo de métodos:

Un método se puede marcar como synchronized para que un solo hilo lo pueda ejecutar a la vez.

public class Contador {
    private int cuenta = 0;

    public synchronized void incrementar() {
        cuenta++;
    }

    public synchronized int getCuenta() {
        return cuenta;
    }
}

En este caso:

  • Solo un hilo puede ejecutar incrementar() o getCuenta() en un momento dado para el mismo objeto.
  • El bloqueo se aplica al objeto actual (this).

b) Bloqueo de bloques de código:

En lugar de sincronizar un método completo, puedes sincronizar un bloque de código dentro de un método.

public class Contador {
    private int cuenta = 0;

    public void incrementar() {
        synchronized (this) {
            cuenta++;
        }
    }

    public int getCuenta() {
        synchronized (this) {
            return cuenta;
        }
    }
}

Aquí, el bloqueo se aplica solo al fragmento sincronizado, no al método completo. Esto mejora el rendimiento al reducir la sección de código bloqueada.


2. Bloqueo en nivel de clase (synchronized static)

Si varios hilos acceden a un método estático de una clase, se puede sincronizar a nivel de clase para evitar conflictos.

public class Banco {
    private static int fondosTotales = 0;

    public static synchronized void agregarFondos(int monto) {
        fondosTotales += monto;
    }

    public static synchronized int getFondosTotales() {
        return fondosTotales;
    }
}

En este caso:

  • Se usa un bloqueo en el objeto Class asociado con la clase Banco.
  • Esto garantiza que no haya acceso simultáneo a estos métodos estáticos.

3. Bloqueo explícito con java.util.concurrent.locks

La API java.util.concurrent.locks ofrece una forma más flexible de controlar el acceso a recursos compartidos. La clase más común es ReentrantLock.

a) Uso básico de ReentrantLock:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Contador {
    private int cuenta = 0;
    private final Lock bloqueo = new ReentrantLock();

    public void incrementar() {
        bloqueo.lock(); // Adquirir el bloqueo
        try {
            cuenta++;
        } finally {
            bloqueo.unlock(); // Liberar el bloqueo
        }
    }

    public int getCuenta() {
        bloqueo.lock();
        try {
            return cuenta;
        } finally {
            bloqueo.unlock();
        }
    }
}

b) Ventajas de ReentrantLock sobre synchronized:

  • Intenta adquirir el bloqueo: Puedes usar tryLock() para intentar obtener el bloqueo sin quedar bloqueado indefinidamente.
  • Bloqueos condicionales: ReentrantLock permite usar condiciones (como await() y signal()) para manejar escenarios más complejos.

4. Bloqueo de objetos específicos

Puedes sincronizar cualquier objeto como monitor de bloqueo:

public class Contador {
    private int cuenta = 0;
    private final Object lock = new Object();

    public void incrementar() {
        synchronized (lock) {
            cuenta++;
        }
    }

    public int getCuenta() {
        synchronized (lock) {
            return cuenta;
        }
    }
}

Aquí, el bloqueo no está vinculado al objeto actual (this) ni a la clase. Es útil si necesitas sincronizar diferentes partes del programa con diferentes bloques.


5. Problemas Comunes de Sincronización

  • Interbloqueos: Ocurren cuando dos o más hilos esperan indefinidamente por bloqueos que nunca se liberan.
  • Condiciones de carrera: Cuando dos hilos acceden simultáneamente a datos compartidos, causando un comportamiento impredecible.
  • Espera activa: Ocurre cuando un hilo queda bloqueado esperando un recurso, gastando CPU innecesariamente.