viernes, 20 de abril de 2012

Threads II - Synchronized

votar

En la entrada anterior hablabamos de los threads, y veíamos las operaciones que podíamos realizar para ordenar el tiempo de ejecución. En esta entrada vamos a tratar de la sincronización, y lo vamos a hacer con un ejemplo ficticio muy sencillito, para que los conceptos queden bien claros.

Supongamos que tenemos un bonito comercio que dispone de una página web donde se pueden hacer compras on-line. Pongamos que entre las cosas que se pueden comprar están CD´s. Nosotros, mañosos que somos, hemos hecho un programa en el que cada vez que un cliente compra un CD, este se resta a las existencias, y cuando ya no quedan, avisa de la situación:


//¡Método no sincronizado, va a fallar!
public class CompraCD{
    
   public static void main(String[] args) throws InterruptedException{
      Comprador c = new Comprador();
      Thread uno = new Thread(c);
      Thread dos = new Thread(c);
      Thread tres = new Thread(c);
      uno.setName("Elena");
      dos.setName("Luis");
      tres.setName("Alba");
      uno.start();
      dos.start();
      tres.start();
   }

     static class Comprador implements Runnable{
      int cantidad = 100;
      public void run(){
         while(cantidad > 0){
            compra();
         }
      }

      public  void compra(){
            if(cantidad>0){
           System.out.println("Comprador " + Thread.currentThread().getName() + " encargando CD");
   /*Vamos a ralentizar un poco el proceso para que se vea más claramente el output*/
               try{
                  Thread.sleep(1000);/
               }catch(InterruptedException e){
                  System.out.println("Oh, oh");
               }
               cantidad = cantidad – 1; //restamos 1 a las existencias
          System.out.println("Comprador " + Thread.currentThread().getName() + " ha comprado. Quedan " + cantidad); //Vemos quién ha comprado y la cantidad de CD´s que quedan
            }else{
               System.out.println("No hay bastantes CD´s");
            }
         
      }
   }
   
}



Ahora podéis probar a compilar y ejecutar el programa. ¿Cuál es el output?Ya os lo digo yo: un desastre. Terminaréis con números negativos en las existencias de CD´s y es posible incluso que el número de CD´s no disminuya de forma correcta, si no que haga cosas como 20-18-19. ¿Por qué?, os preguntaréis, si en el programa se especifica claramente que sólo se puede comprar mientras haya existencias. Pues porque puede haber más de una persona comprando a la vez. Suponed que sólo queda un CD y dos personas entran a la vez en la web: a las dos se les va a decir que hay existencias, y las dos van a poder realizar la compra, pero sólo vamos a tener un CD para enviar.

Vale, en la vida real el proceso no es exactamente así, pero ya dije que esto era sólo un ejemplo para que lo entendiérais. Ahora, lo que necesitamos, es una forma de impedir que dos personas compren a la vez, es decir, que mientras una persona está comprando, otra no pueda iniciar el proceso. Para eso usamos la sincronización. Bastará con añadir esta simple palabra: synchronized en el método de esta manera:


   public void synchronized compra(){}

y a partir de ahí, si un thread lo está ejecutando, los demás no lo podrán usar hasta que el otro termine. Probad ahora a compilar y ejecutar el programa y veréis como ya no vende más CD´s de los que hay.

Recordad que lo que se sincroniza son los métodos, no las clases. También puede ocurrir que tengamos un método más amplio y no necesitemos sincronizarlo todo, si no que haya partes de él que no importa si varios thread ejecutan a la vez. En ese caso, bastará con que sincronicemos el bloque que nos interesa:



public class CuentaAtras extends Thread{
    private int cuenta  = 20;
    public  void contador(){
         synchronized(this){ 
            for(int i=0; i<10; i++){
                  cuenta--;
                 System.out.println(Thread.currentThread().getName()+": "+cuenta);
            }
        }
       System.out.println("No sincronizado");
    }
   

    public void run(){ 
       contador();
    }

    public static void main (String[] args){
      CuentaAtras cd = new CuentaAtras();
      Thread t1 = new Thread(cd);
      Thread t2 = new Thread(cd);
      t1.start();
      t2.start();
   }
}

La salida de este código será la siguiente:
Thread-1: 19
Thread-1: 18
Thread-1: 17
Thread-1: 16
Thread-1: 15
Thread-1: 14
Thread-1: 13
Thread-1: 12
Thread-1: 11
Thread-1: 10
Thread-2: 9
Thread-2: 8
Thread-2: 7
Thread-2: 6
Thread-2: 5
Thread-2: 4
Thread-2: 3
Thread-2: 2
Thread-2: 1
Thread-2: 0
No sincronizado
No sincronizado //Estas dos últimas líneas pueden variar

Vemos que el primer thread se ejecuta completamente antes de que empiece el segundo, y como cuando empieza el segundo la cuenta atrás ha ido desde el número 19 hasta el 10, él empieza en el 9.

La sincronización es como una llave que permite entrar a un thread en un bloque de código, y mientras ese thread tiene la llave, ningún otro thread puede entrar en él. De hecho, todos los objetos en java tienen un lock o cerradura, también llamado monitor. Pero eso es otra historia.

Esperamos que estas nociones básicas sobre threads y sincronización os hayan sido útiles.

Nos gustaría agradecer especialmente a los amables habitantes de JavaRanch por su ayuda en la elaboración de esta entrada.

6 comentarios:

  1. public void by the grace of God()30 de abril de 2012, 1:27

    Muy buen blog. Sigue así, me estás ayudando mucho.

    Saludos.

    ResponderEliminar
  2. Muchas gracias por tus palabras. Para eso estamos. Y no dudéis en sugerir temas que queráis que se traten.
    Saludos.

    ResponderEliminar
  3. Hay algunos errores en ese algoritmo. No creo que de ese resultado de 20 a 0, ya que la variable "cuenta" se inicializa en 10 en lugar de 20 y el bucle for debe ser de 10 en lugar de 20, por ultimo, la variable count no está declarada.

    ResponderEliminar
    Respuestas
    1. ¡Muchas gracias Maver | ck, por estar al quite de mis errores tipográficos! Efectivamente, tenía cambiados de lugar el 10 y el 20, y la segunda vez había escrito la variable cuenta en inglés (a veces me pasa).¡Creo que a partir de ahora voy a empezarme a hacer Copia y Pega a mí misma, en lugar de volver a escribir el código!
      Gracias a ti, ya lo he corregido. Por favor, no dudes en avisarme si ves que meto la pata en algún otro sitio. Me temo que lo de la revisión no es lo mío.
      ¡Un saludo! ;-)

      Eliminar
  4. Hola Sonia, vuestros tutoriales son excelentes, pero tengo la siguiente duda:

    Supongamos que tengo las clases Hilo1 e Hilo2 cada clase con un método run diferente, una imprime un mensaje y la otra hace otra cosa (cualquier otra cosa), y en el main inicializo los dos hilos, ahora, ¿cómo hago para sincronizar estos hilos? que por ejemplo en determinado momento el Hilo1 se pare o se "sincronize" mientras el Hilo2 se ejecuta, sabiendo que cada hilo corresponde a una clsae "runnable" diferente

    ResponderEliminar
    Respuestas
    1. Hola, Sebastián.
      Ante todo, muchas gracias por tu amabilidad. En cuanto a tu pregunta, en realidad no son los hilos los que se sincronizan, sino lo que tengan en común. Supongamos que esos hilos necesiten acceder a un mismo objeto (unos datos, un fichero, etc.), entonces es a ese objeto al que hay que añadirle la palabra clave synchronized, de forma que sólo un hilo pueda acceder a él en un momento dado, y que las actualizaciones que se hagan en el mismo sean visibles para los dos.
      Si por otro lado, lo que tienen en común es un método (por ejemplo, los dos utilizan el método imprimir()) lo que debes sincronizar es el método (dentro de run, no de main).
      ¿Y qué ocurre si son totalmente independientes y no comparten ni métodos, ni datos ni nada, aunque necesitas que se ejecuten a la vez, pero que en un determinado momento uno espere a que acabe el otro?
      Entonces no necesitas sincronizar nada, sino que puedes utilizar por ejemplo join() (hecha un ojo al tutorial anterior a este, Threads I) Por ejemplo, si la instancia de Hilo2 es h2, con h2.join(), h1, instancia de Hilo1 esperará a que termine de ejecutarse el otro hilo antes de continuar su propia ejecución.
      Ten en cuenta además que aquí hablamos de un manejo de hilos muy básico, simplemente para entender los conceptos, pero que para códigos más complejos, del mundo real, lo mejor es utilizar un "ejecutor" (Executor) Puedes leer sobre ellos aquí: http://docs.oracle.com/javase/tutorial/essential/concurrency/exinter.html
      Espero haberte sido de ayuda.
      Un saludo.

      Eliminar