Java Sincronizacion Conceptos Básicos

La sincronización de nuestro código evitará que diferentes hilos accedan a la misma vez a determinada porción de código.

Este es un problema tipico de concurrencia.

Suponemos este caso, donde dos personas con una cuenta en común que tiene balance de 80, realizan a la misma vez una extración por 50. La cuenta no debería quedar sobregirada, pero en este caso, al realizar la extración de forma ‘simultenea’ se origina este sobregiro no deseado.

public class AccountService implements Runnable {

    private Account acct = new Account(80);

    public static void main(String[] args) {

        AccountService r = new AccountService();
        Thread one = new Thread(r);
        Thread two = new Thread(r);
        one.setName("Fred");
        two.setName("Lucy");

        one.start();
        two.start();

    }

    public void run() {

        makeWithdrawal(50);
        if (acct.getBalance() < 0) {
            System.out.println("account is overdrawn ! " + acct.getBalance());
        }
    }

    private void makeWithdrawal(int amt) {
        if (acct.getBalance() >= amt) {
            System.out.println(Thread.currentThread().getName() + " is going to withdraw");
            acct.withdraw(amt);
            System.out.println(Thread.currentThread().getName() + " completes the withdrawal");
        } else {
            System.out.println("Not enough in account for " + Thread.currentThread().getName() + " to withdraw "
                    + acct.getBalance());
        }
    }
}

public class Account {

    private int balance;

    Account(int initialBalance) {
        this.balance = initialBalance;
    }

    int getBalance() {
        return balance;
    }

    void withdraw(int amount) {
        balance = balance - amount;
    }

}

La salida de este código será:

    Fred is going to withdraw
    Fred completes the withdrawal
    Lucy is going to withdraw
    Lucy completes the withdrawal
    account is overdrawn ! -20

Cada objeto en Java tiene un lock que podemos utilizar cuando sincronizamos nuestro codigo.

Si sincronizamos nuestro método adquiriremos el lock asociado a la instancia de la clase que estamos ejecutando.

Sincronizando nuestro metodo ‘makeWithdrawal(..)’ tendremos algo así:

private synchronized void makeWithdrawal(int amt) {
        if (acct.getBalance() >= amt) {
            System.out.println(Thread.currentThread().getName() + " is going to withdraw");
            acct.withdraw(amt);
            System.out.println(Thread.currentThread().getName() + " completes the withdrawal");
        } else {
            System.out.println("Not enough in account for " + Thread.currentThread().getName() + " to withdraw "
                    + acct.getBalance());
        }
    }

Ahora la salida de esta código será la siguiente, donde podemos ver que el problema ha sido resuelto evitando que dos hilos ingresen en forma simultanea:

    Fred is going to withdraw
    Fred completes the withdrawal
    Not enough in account for Lucy to withdraw 30

Hay un solo lock por objeto y un thread puede tomar ese lock a la vez. Es decir, ningún otro hilo puede tomar el lock hasta que el primero lo libere.

Hay que tener en cuenta que:

  • Solo es posible sincronizar metodos y bloques de código.
  • Cada objeto tiene un solo lock
  • El lock es por la instancia de la clase. Dos instancias diferentes tendrán dos lock diferentes, aunque se trate del mismo método.
  • Si un hilo queda en sleep, esto hará que el lock quede en espera, es decir no sera liberado hasta que termine el sleep.
  • Un thread puede adquirir mas de un lock para diferentes objetos. Por ejemplo, puede ejecutar un metodo sincronizado de un objecto y luego otro metodo sincronizado de otro objeto, aquiriendo así dos bloqueos; uno por cada objeto.

Podríamos sincronizar el bloque del código, en vez del método, con el mismo resultado:

public synchronized void doStuff() {
    System.out.println("synchronized"); 
}

public void doStuff() {
    synchronized(this) {
        System.out.println("synchronized"); 
    }
}

Cuando sincronizamos el método, el lock será el del objeto del método. Pero si sincronizamoe el bloque somos nosotros los que decidimos que objeto utilizar como lock. En el ejemplo anterior utilizamos el objeto del mótodo (this) pero podriamos utilizar alguno otro:

Por ejemplo:

public void doStuff() {
    synchronized(anotherObject) {
        // en este caso el lock estara sobre anotherObject
        System.out.println("synchronized"); 
    }
}

Sincronizar metodos static:

En el caso de los metodos estaticos, al ser un metodo de clase, el lock sera para la clase.

Recordar en que en los metodos NO estaticos el lock es para la instancia. Para una misma clase con dos instancias diferentes, cada instancia tendrá un lock para su respectiva instancia, pero trátandose de un metodo estatico el lock será un lock de clase.

public static synchronized int getCount() {
    return count;
} 

// es lo mismo tambien así.. 

public static int getCount() {
   synchronized(MyClass.class) {
       return count;
   }
} 

Ref: scjp certified programmer

Hi! If you find my posts helpful, please support me by inviting me for a coffee :)

Ver también