Mecanismos Internos de synchronized en Java

La implementación de synchronized en Java se basa en el concepto de Monitores (o cerraduras de monitor) y la información almacenada en la Cabeza del Objeto (Object Header).

Funcionamiento de synchronized

Bloques synchronized

Cuando se utiliza synchronized para proteger un bloque de código, el compilador de Java inserta las instrucciones monitorenter y monitorexit antes y después del bloque, respectivamente. La instrucción monitorenter puede entenderse como la adquisición de un bloqueo, mientras que monitorexit representa su liberación.

  • El objeto Monitor está contenido en la "Cabeza del Objeto" de cada instancia de Java. Esto explica por qué cualquier objeto en Java puede ser utilizado como una cerradura (lock).
  • El Monitor interno tiene un contador. Si el contador es 0, el hilo puede adquirir el bloqueo exitosamente y el contador se incrementa a 1.
  • Al ejecutar monitorexit, el contador se decrementa. Si el contador llega a 0, el bloqueo se libera.
  • Si un hilo falla al adquirir el bloqueo, entrará en estado de espera (bloqueado) hasta que otro hilo libere el bloqueo.

Métodos synchronized

Los métodos declarados como synchronized no utilizan las instrucciones explícitas monitorenter y monitorexit. En su lugar, la JVM utiliza el indicador ACC_SYNCHRONIZED en la tabla de constantes del método. Cuando se invoca un método con este indicador, la JVM verifica si el hilo puede adquirir el monitor asociado al objeto (o clase, en el caso de métodos estáticos) antes de ejecutar el cuerpo del método.

Usos y Propiedades de synchronized

Usos Comunes

  1. Métodos de Instanica (Normal Methods): El bloqueo se aplica sobre la instancia actual del objeto. Para entrar al bloque sincronizado, el hilo debe adquirir el bloqueo de esa instancia.
  2. Métodos Estáticos (Static Methods): El bloqueo se aplica sobre el objeto Class de la clase. Esto significa que todos los hilos que intenten acceder a métodos estáticos sincronizados de esa clase compartirán el mismo bloqueo, independientemente de cuántas instancias se creen.
  3. Bloques de Código (Code Blocks): Permite especificar explícitamente el objeto que se utilizará como bloqueo. El hilo debe adquirir el bloqueo del objeto especificado para entrar al bloque.

Beneficios de synchronized

  • Atomicidad: Asegura que solo un hilo a la vez pueda acceder a la sección de código sincronizado, evitando condiciones de carrera.
  • Visibilidad: Garantiza que las modificaciones realizadas a variables compartidas por un hilo sean visibles para otros hilos.
  • Ordenamiento: Ayuda a prevenir problemas de reordenamiento de instrucciones por parte del compilador o la CPU en entornos multihilo.

Diferencias Clave

Métodos Estáticos vs. Métodos de Instancia

  • synchronized static: Bloquea el objeto Class de la clase. Todas las instancias de la clase comparten este único bloqueo.
  • synchronized (método de instancia): Bloquea la instancia específica del objeto sobre la que se llama el método. Cada instancia tiene su propio bloqueo independiente.

¿Se puede usar synchronized en Constructores?

Los constructores no pueden ser declarados como synchronized. Sin embargo, es posible usar bloques synchronized dentro de un constructor. Los constructores, por sí mismos, se consideran seguros para hilos en el sentido de que la creación de un objeto no es visible para otros hilos hasta que la construcción se completa. No obstante, si un constructor interactúa con recursos compartidos, se deben aplicar medidas de sincronización adecuadas.

volatile vs. synchronized

| Característica | volatile | synchronized | | :-------------------- | :-------------------------------------- | :---------------------------------------- | | Ámbito | Solo variables | Clases, variables, métodos, bloques | | Garantías | Visibilidad | Atomicidad, Visibilidad | | Reordenamiento | Impide reordenamiento | No impide completamente, garantiza orden | | Bloqueo | No bloquea hilos | Puede bloquear hilos | | Operaciones | Acceso directo a memoria | Utiliza monitores (basado en objeto) | | Rendimiento (general) | Más ligero | Más pesado |

synchronized y Reordenamiento de Instrucciones

synchronized no prohíbe completamente el reordenamiento de instrucciones dentro del bloque sincronizado si dicho reordenamiento no viola la semántica de un solo hilo. Sin embargo, a través de las barreras de memoria implícitas, garantiza la ordenación en un contexto multihilo. Para una prohibición estricta del reordenamiento, volatile es una mejor opción.

ReentrantLock vs. synchronized

| Característica | synchronized | ReentrantLock | | :-------------------- | :------------------------------------------- | :------------------------------------------ | | Liberación del Lock | Automática al salir del bloque/método | Manual (unlock()) | | Equidad (Fairness) | No es justo (non-fair) | Puede ser justo (fair) o injusto | | Interrupción | Espera indefinida | Hilos esperando pueden ser interrumpidos | | Time-out | No | Sí (tryLock(long timeout, ...)) | | Bloqueo No Bloqueante | No | Sí (tryLock()) | | Reentrada | Sí (es reentrante) | Sí (es reentrante) |

Reentrant Locks (Cerraduras Reentrantes)

Una cerradura reentrante permite que un hilo que ya posee el bloqueo pueda adquirirlo nuevamente sin bloquearse a sí mismo. Esto es útil en escenarios de llamadas anidadas o recursividad.

synchronized es intrínsecamente una cerradura reentrante. La JVM maneja un contador de recursión para cada objeto bloqueado por un hilo.

Principio de Reentrancia

Cuando un hilo adquiere un bloqueo synchronized, se incrementa un contador en el objeto Monitor asociado. Si el mismo hilo intenta adquirir el bloqueo nuevaemnte, el contador se incrementa. El bloqueo solo se libera cuando el contador vuelve a cero (es decir, cuando el hilo ha ejecutado el número correspondiente de monitorexit).

Ejemplo de Código (Conceptual):


public class ReentrantExample {
    private final Object lock = new Object();

    public void outerMethod() {
        synchronized (lock) {
            System.out.println("Adquiriendo bloqueo en outerMethod...");
            innerMethod(); // Llamada anidada
            System.out.println("Liberando bloqueo en outerMethod...");
        }
    }

    public void innerMethod() {
        synchronized (lock) {
            System.out.println("Adquiriendo bloqueo en innerMethod...");
            // Hacer algo más...
            System.out.println("Liberando bloqueo en innerMethod...");
        }
    }

    public static void main(String[] args) {
        ReentrantExample example = new ReentrantExample();
        new Thread(example::outerMethod).start();
    }
}

Ventajas de la Reentrancia

  • Prevención de Deadlocks: Evita que un hilo se bloquee a sí mismo al intentar re-adquirir un bloqueo que ya posee.
  • Simplificación del Código: Permite la modularidad, como llamar a métodos sincronizados desde otros métodos sincronizados sin preocuparse por el estado del bloqueo.
  • Encapsulación: Facilita la creación de código reutilizable donde las operaciones internas pueden estar sincronizadas sin exponer la necesidad de bloqueo externo.

Garantía de Visibilidad con synchronized

La visibilidad se garantiza a través de la regla del Monitor del modelo de memoria Happens-Before:

  • La liberación de un monitor (monitorexit) en un hilo happens-before la adquisición de ese mismo monitor (monitorenter) por otro hilo.

Esto significa que todos los cambios realizados por el hilo que libera el bloqueo se hacen visibles para el hilo que adquiere el bloqueo posteriormente. El bloque synchronized actúa como una barrera de memoria, asegurando que las escrituras dentro del bloque sean visibles antes de que otro hilo pueda leerlas después de adquirir el mismo bloqueo.

Ejemplo Ilustrativo:


public class VisibilityExample {
    private volatile int counter = 0; // Usamos volatile aquí para énfasis, synchronized también lo garantiza

    // synchronized garantiza la visibilidad de 'counter++' para el método reader()
    public synchronized void increment() {
        counter++;
    }

    public synchronized int getValue() {
        return counter;
    }

    public static void main(String[] args) {
        VisibilityExample example = new VisibilityExample();

        // Hilo 1: Incrementa el contador
        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        }).start();

        // Hilo 2: Lee el valor del contador
        new Thread(() -> {
            // Espera un poco para que el hilo 1 tenga tiempo de ejecutar
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Valor leído por Hilo 2: " + example.getValue());
        }).start();
    }
}

En el ejemplo anterior, la llamada a getValue() por el segundo hilo leerá un valor actualizado debido a la garantía de visibilidad proporcionada por synchronized en ambos métodos.

Evolución de los Bloqueos (Lock Upgrade)

Desde Java 6, synchronized ha incorporado varias optimizaciones para mejorar el rendimiento, como:

  • Bloqueo de Orientación Predominante (Biased Locking): Asume que un bloqueo rara vez será disputado y lo otorga al primer hilo que lo adquiere sin costo adicional.
  • Bloqueo Ligero (Lightweight Locking): Utiliza CAS (Compare-And-Swap) para permitir que un hilo adquiera el bloqueo sin necesidad de bloquear el sistema operativo si no hay contención.
  • Spinning (Giro): Si un hilo no puede adquirir un bloqueo ligero, en lugar de bloquearse inmediatamente, puede "girar" (esperar activamente) por un corto período, asumiendo que el bloqueo se liberará pronto.

Estos mecanismos permiten que el bloqueo evolucione a través de diferentes estados según la contención:

  1. Sin Bloqueo (Un-locked): Estado inicial.
  2. Bloqueo Predominante (Biased Locked): Optimizado para un solo poseedor.
  3. Bloqueo Ligero (Lightweight Locked): Utiliza CAS.
  4. Bloqueo Pesado (Heavyweight Locked): Requiere la intervención del sistema operativo (bloqueo real) cuando la contención es alta.

Esta evolución tiene como objetivo minimizar la sobrecarga en escenarios de baja contención y escalar a bloqueos más robustos cuando es necesario.

Si un bloqueo pesado se libera y no hay más competencia, puede volver al estado de "sin bloqueo" o incluso al "bloqueo predominante" si un nuevo hilo lo adquiere. El proceso de actualización es unidireccional: de menos costoso a más costoso.

Etiquetas: java synchronized monitor jvm concurrencia

Publicado el 7-31 06:52