En posts pasados habiamos estudiado como usar correctamente threads en java. Pero cualquier esfuerzo por usar threads correctamente en este lenguaje se queda corto si no sincronizamos nuestros threads correctamente.

Vamos a analizar el típico caso de uso que demuestra la importancia de que nuestros threads sean sincronizados:

Dado que tenemos cuentas bancarias en un sistema
Y varias personas pueden depositar o retirar al mismo tiempo de una misma cuenta
El balance de la cuenta debería ser consistente con las operaciones que los clientes realizan

Es decir, que si tenemos la cuenta X con un saldo de $300 y el cliente A retira $200 al mismo tiempo que el cliente B retira $200 también, al menos uno de los dos debería recibir un mensaje de saldo insuficiente.

Para entender un poco mejor cómo vamos a solucionar esto primero veamos un diagrama de bloques de como van a interactuar las partes del sistema:

Ahora definimos las 3 clases que van a interactuar en el sistema.

Banco.java

package com.ricardogeek;

import java.util.HashMap;

public class Banco {
	private final static Banco instancia = new Banco();
	private HashMap<Integer, CuentaBancaria> cuentas;

    private Banco() {
    	cuentas = new HashMap<Integer, CuentaBancaria>();
    	cuentas.put(123456, new CuentaBancaria(123456));
    }

    public static Banco getInstance(){
        return instancia;
    }

    public CuentaBancaria getCuenta(final Integer numero) {
        return cuentas.get(numero);
    }
}

El objeto banco que contiene varias cuentas bancarias.

CuentaBancaria.java

package com.ricardogeek;

public class CuentaBancaria {
      private Integer balance;
      private Integer numero;

      public CuentaBancaria(final Integer numero, final Integer balance) {
        this.numero = numero;
        this.balance = balance;
      }

      public CuentaBancaria(final Integer numero) {
        this(numero, 0);
      }

      public Integer getBalance() {
        return balance;
      }

      public Integer getNumero() {
        return numero;
      }

      public void depositar(final Integer monto) {
            this.balance = this.balance + monto;
            System.out.println(Thread.currentThread().getName() + " Depositando el monto: " + monto + " El nuevo Balance Es:  " + this.balance);
      }

      public Integer retirar(final Integer monto) {
        System.out.println(Thread.currentThread().getName() + " Intentado Retirar " + monto + " Del numero de cuenta: " + this.numero);

        if (balance < monto) {
          System.out.println("ERROR: BALANCE INSUFICIENTE: " + Thread.currentThread().getName());
          return 0;
        }

        this.balance = this.balance - monto;
        System.out.println(Thread.currentThread().getName() + " Retiro exitoso. El nuevo balance es:  " + this.balance);
        return monto;
      }
}

Notamos que cuenta bancaria tiene los métodos de interés depositar que le suma un valor al balance general de la cuenta y retirar que le resta un valor al balance general de la cuenta, Que pasa cuando estas dos operaciones se ejecutan al mismo tiempo? Ya lo veremos 😎

Cliente.java

package com.ricardogeek;

public class Cliente implements Runnable {

    @Override
    public void run() {
        Banco banco = Banco.getInstance();
        CuentaBancaria cuenta = banco.getAccount(123456);
        cuenta.depositar(100);
        cuenta.retirar(200);
    }
}

Finalmente el cliente que, para efectos de ejemplificar, al correr hace 2 operaciones: deposita $100, y luego inmediatamente retira $200

Ahora bien, aquí viene lo bueno, vamos a hacer una clase principal que esencialmente hará un desastre 🙂

package com.ricardogeek;

public class Main {
    public static void main(String[] args) {
        Cliente clienteA = new Cliente();
        Cliente clienteB = new Cliente();
        Thread hilo1 = new Thread(clienteA);
        Thread hilo2 = new Thread(clienteB);
        hilo1.setName("Cliente-A");
        hilo2.setName("Cliente-B");
        hilo1.start();
        hilo2.start();
    }
}

Si ejecutamos la clase Main unas cuantas veces, vemos el horror:

Intento 1:

Cliente-A Depositando el monto: 100 El nuevo Balance Es:  200
Cliente-B Depositando el monto: 100 El nuevo Balance Es:  200
Cliente-A Intentado Retirar 200 Del numero de cuenta: 123456
Cliente-B Intentado Retirar 200 Del numero de cuenta: 123456
Cliente-A Retiro exitoso. El nuevo balance es:  0
ERROR: BALANCE INSUFICIENTE: Cliente-B

Intento 2:

Cliente-B Depositando el monto: 100 El nuevo Balance Es:  200
Cliente-A Depositando el monto: 100 El nuevo Balance Es:  200
Cliente-B Intentado Retirar 200 Del numero de cuenta: 123456
Cliente-A Intentado Retirar 200 Del numero de cuenta: 123456
Cliente-B Retiro exitoso. El nuevo balance es:  0
ERROR: BALANCE INSUFICIENTE: Cliente-A

Noten que después de los depósitos no importando que, el balance es 200 cuando el primero debería ser 100, y el segundo 200, por lo tanto el retiro de cliente-A debería fallar el 100% de las veces y el retiro del cliente-B debería ser exitoso el 100% de las veces. Noten también el orden impredecible de las operaciones de cliente-A y cliente-B

Para solucionar este lío, es necesario que antes de realizar las operaciones sobre la cuenta creemos un bloqueo sobre el objeto de modo que nadie lo pueda usar hasta que terminemos nosotros de usarlo. En lenguajes como C/C++ esto es un verdadero reto! pero en java lo logramos fácilmente.

Simplemente vamos a modificar nuestro método principal usando esta vez un bloque sincronizado con la palabra reservada synchronized:

public class Cliente implements Runnable {

    @Override
    public void run() {
        Banco banco = Banco.getInstance();
        CuentaBancaria cuenta = banco.getCuenta(123456);
        
        synchronized(cuenta) {
        	cuenta.depositar(100);
        	cuenta.retirar(200);
        }
    }
}

Ahora si ejecutamos este código, el 100% de las veces obtenemos lo siguiente:

Cliente-A Depositando el monto: 100 El nuevo Balance Es:  100
Cliente-A Intentado Retirar 200 Del numero de cuenta: 123456
ERROR: BALANCE INSUFICIENTE: Cliente-A
Cliente-B Depositando el monto: 100 El nuevo Balance Es:  200
Cliente-B Intentado Retirar 200 Del numero de cuenta: 123456
Cliente-B Retiro exitoso. El nuevo balance es:  0

Y así, es como sin mucho esfuerzo sincronizamos nuestros threads en java.

Categorized in: