Skip to content

1 Paquete atomic

Paquete java.util.concurrent.atomic

En un tema anterior vimos que si tenemos una variable que puede ser leída y escrita desde distintos hilos deberemos incorporar algún mecanismo que asegure la exclusión mutua, como synchronize o ReentrantLock. El simple hecho de poder incrementar una variable entera desde distintos hilos ya nos obliga a incorporar alguno de estos mecanismos.

Sin embargo, debemos tener en cuenta que el empleo de estas herramientas de sincronización implica una ralentización del tiempo de ejecución de nuestro código, debido al tiempo empleado por estas herramientas para asegurar la integridad.

Con objeto de solucionar este problema es casos tan simples como el descrito anteriormente, Java 5 incorporó el paquete java.util.concurrent.atomic, que incluye clases wrapper (envoltura) que permiten mantener la integridad de un determinado tipo de dato sin tener que usar sincronización.

Para que estas clases puedan asegurar la integridad hacen uso de una operación a bajo nivel proporcionada por el procesador denominado compare and swap, o también compare and set (CAS), que recibe el valor que se espera que tenga actualmente una variable y el nuevo valor que se le pretende asignar, y sólo se realiza la asignación si el valor que se espera que tenga actualmente es realmente el que contiene.

Esta técnica es útil es operaciones del tipo check then act (comprobar y luego actuar). El problema de este tipo de operaciones en programas multihilo es que debemos asegurar que desde que se comprueba hasta que se actúa no ha cambiado el valor.

Veamos un ejemplo de operación que puede dar problemas en programas multihilo:

public class Count {

    private volatile long count = 0;

    public void increment() {
       count++;
    }

}

Para asegurar la atomicidad de la operación de incremento podemos usar, por ejemplo synchronized:

public class Count {

    // No hace falta definirla volatile porque synchronized asegura también la
    // visibilidad.
    private long count = 0;

    public synchronized void increment() {
       count++;
    }

}

Sin embargo, tenemos una manera más óptima de asegurar la atomicidad del incremento sin tener que usar algún mecanismo de sincronización. La solución consiste en usar una de las clases incluidas en el paquete java.util.concurrent.atomic, en este caso AtomicLong. Esta clase envuelve un valor entero proporcionando la funcionalidad del mantenimiento de la integridad mediante el empleo de la operación de bajo nivel compare and swap del procesador. Así, modificaremos nuestro código de la siguiente manera:

public class Count {

    // No hace falta definirla volatile porque AtomicInteger usa internamente
    // una variable volatile.
    private AtomicLong count = new AtomicLong(0);

    public void increment() {
       count.incrementAndGet();
    }

}

La operación compare and swap (CAS) es optimista, en el sentido de que se obtiene el valor y luego trata de actualizarla. Si al ir a actualizarla se detecta que el valor existente es igual que el valor que había sido obtenido, entonces simplemente se actualiza, sin que haya sido necesario llevar a cabo ningún tipo de sincronización. Si, por el contrario para cuando se va a realizar la actualización se detecta que el valor actual es distinto al que había sido obtenido, es porque en el periodo de tiempo desde que se obtuvo el valor hasta que se trata de actualizar otro hilo ha cambiado el valor de la variable. En ese caso, no se realiza la actualización, sino que se vuelve a iniciar el proceso de nuevo, es decir obtener el valor y tratar de actualizar (espera activa, busy wait). En algún instante futuro no muy lejano la operación compare and swap podrá ser realizada, y no habrá sido necesario usar ningún tipo de sincronización.

Para comprender la técnica descrita anteriormente, veamos el código interno del método incrementAndGet():

public final long incrementAndGet() {
    for (;;) {
        long current = get();
        // Dangerous zone
        long next = current + 1;
        // End of dangerous zone
        if (compareAndSet(current, next))
          return next;
    }
}

En Java 8 el código anterior se ha cambiado por una versión más reducida (pero el funcionamiento es el mismo):

public final long incrementAndGet() {
    return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}

En realidad el código anterior es convertido a una única instrucción máquina que en el caso de la arquitectura de procesadores x86 corresponde a LOCK XADD, que proporciona un mejor rendimiento que el clásico bucle CAS.