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.

sábado, 14 de abril de 2012

Threads I - Formas de crear threads

votar
   Los threads, llamados en castellano hilos o hebras, son partes del código que se encargan de ejecutar tareas de forma simultánea en varios procesadores o de forma conmutada si se trata de un solo procesador ( aunque en este último caso, siendo las velocidades las que son, parezca que dichas tareas se ejecutan todas a la vez).

   Como este blog esta pensado para novatos, vamos a ir poco a poco y explicar de forma sencilla  y con ejemplos cómo se pueden crear threads en un programa.

   Hay que tener en cuenta que la tarea que ejecutan estas hebras pueden ser la misma, o bien pueden ser tareas distintas. Por ejemplo, supongamos que queremos hallar simultáneamente el factorial de tres números. En este caso lo haremos extendiendo la clase Thread:


public class Factorial extends Thread{
   long nf;
   Factorial(long n){
      nf = n;
   }
   

   public static void main (String[] args){
      Factorial a = new Factorial(10);
      Factorial b = new Factorial(15);
      Factorial c = new Factorial(20);
      a.setName("a");
      b.setName("b");
      c.setName("c");
      a.start();
      b.start();
      c.start();
   }
      public void run(){
         
         long fact = 1;
         while(nf>1){
            fact *= nf--;
         }
         System.out.println("El factorial de " + Thread.currentThread().getName() + " es: "+ fact);
      }
   
}

   Vemos en este ejemplo que hemos extendido la clase Thread y a continuación hemos creado tres instancias de la subclase creada Factorial, cada una de las cuales ejecuta la misma tarea que las demás, pero sobre distintos números. La clase Thread tiene varios métodos interesantes, como setName() y getName(), para nombrar los distintos hilos (aunque por defecto son numerados a partir del 0) y currentThread() para saber que hilo se está ejecutando en un momento preciso. 


   Pero además, la clase Thread implementa la interfaz Runnable, que sólo contiene un método: run().
   El método run() es la tarea que queremos realizar; por ello debemos sobreescribirlo con lo que queremos que hagan nuestros threads. Pero fijaos bien, para que una hebra comience a hacer la tarea que queremos, nunca, jamás, podemos utilizar run().


      a.run(); //¡JAMÁS!


      Lo que usamos es el método start(), como vemos en el ejemplo:


      a.start(); //correcto.


      Este método es el que se encarga de llamar al método run().


      La forma que hemos visto en este ejemplo de crear threads es la más sencilla, pero no es la más habitual. ¿Por qué?. Pues para empezar, porque si extiendes la clase Thread, ya no puedes extender ninguna otra clase, y eso limita bastante. Además, lo correcto es extender una clase sólo cuando se quiere un comportamiento más especializado de la misma.


     La forma de crear hebras que nos va a permitir mayor flexibilidad para trabajar es crear una clase que implemente la interfaz Runnable, como hace la clase Thread, y luego pasar las referencias de sus instancias como argumentos a las instancias de Thread.


      Lo entenderéis más fácilmente con el siguiente ejemplo:






      
public class DemoThread{
   public static void main(String[] args)throws InterruptedException{
      Runnable r1 = new TextoUno(); //creamos una instancia de una clase que implementa Runnable
      Runnable r2 = new TextoDos(); //creamos una instancia de otra clase que también implementa Runnable
   
      /*creamos dos instancias de la clase Thread y le asignamos a cada una "tarea" distinta*/
      Thread t1 = new Thread(r1); 
      Thread t2 = new Thread(r2);

      t1.start(); //t1 comienza su ejecución
      t1.join(); //t1 obliga a t2 a esperar hasta que acaba su ejecución
      t2.start(); //t2 comienza su ejecución
   }
}

class TextoUno implements Runnable{
   public void run(){ //sobreescribimos el método run()
      System.out.println("Esto es una demostración");
      for(int i=0; i<10; i++){
         System.out.println("Estamos esperando...");
         try{
            Thread.sleep(1000); //ralentizamos la ejecución
         }catch (InterruptedException e){
            System.out.println("¡Madre mía!");
         }
      }
   }
}

class TextoDos implements Runnable{
   public void run(){ //sobreescribimos el método run() de forma distinta
      System.out.println(Thread.currentThread().getName());
      System.out.println("Por fin acabó t1");
   }
}

    En este segundo ejemplo tenemos dos hilos, cada uno de los cuales hace una cosa distinta. Pero, atención: el orden en el que nosotros iniciamos los hilos no es necesariamente el orden que ellos van a seguir. 


      Imaginemos que tenemos dos hilo: t1, que tiene como salida los números del 1 al 10, y t2, que tiene como salida las vocales, e iniciamos primero t1 y luego t2. Puede que nos salgan primero los números y después las vocales, o puede que nos salga cualquier mezcla entre números y vocales. Es más,  si volvemos a ejecutar el código, el resultado no tiene porqué ser el mismo. Entonces, ¿cómo hacemos si queremos que los hilos se ejecuten en un orden específico?. Hay varias formas de conseguirlo. La más eficaz es con el método join() que hemos utilizado aquí. Cuando este método es llamado por un hilo le indica a los demás que tienen que esperar a que dicho hilo acabe. En un pr0grama tan sencillo como este, lo hemos aplicado a toda la ejecución, pero en otros más complejos se puede dejar que los hilos comienzan simultáneamente y se entrecrucen, y usarlo en el momento necesario de la ejecución.


      Hay otras formas, no tan eficaces, de intentar "manejar los hilos" (mal juego de palabras, ya sé). Puede usarse por ejemplo el método sleep() que pausa la ejecución de un hilo durante unos milisegundos o nanosegundos. Tanto el método sleep() como el método join() lanzan una InterruptedException, que hay que declarar o manejar. Si no entiendes esto, hecha un vistazo a la entrada sobre excepciones en java.


   También se pueden establecer prioridades, que pueden o no funcionar como el programador espera. La manera de hacerlo es con el método setPriority( int priority) con valores del 1 al 10. O se pueden usar las constantes Thread.MAX_PRIORITY, Thread.MIN_PRIORITY y Thread.NORM_PRIORITY. Pero esto tampoco es muy fiable y depende mucho del sistema operativo que se use.


  La moraleja de todo esto, es que nunca des por sentado cómo se van a comportar los threads, o que porque tu programa funcione de una manera en un procesador, funcionará igual en todos. Así que asegúrate de que tu programa multihebra no depende de estos factores. 


   Y si esta falta de control te parece irritante, espera a ver nuestra próxima entrada, que trata de la sincronización.


   Si deseas más información, puedes consultar el libro de Scott Oaks y Henry Wong, Java Threads, publicado por O´Reilly, o los Tutoriales de Oracle.


   

viernes, 6 de abril de 2012

Anotaciones en Java

votar
   Esta entrada va a ser básicamente teórica, porque hoy vamos a hablar de las anotaciones en Java.
   Las anotaciones son una especie de comentarios o metadatos que puedes introducir en tu código, pero que no son en realidad parte del programa. Puedes elegir que sean procesadas durante la compilación o bien durante la ejecución, a través del API de Reflection. La regla de oro a seguir es que el programa debe funcionar igual tanto si le añades como si le quitas las anotaciones.

   Las anotaciones se escriben siempre antes del elemento al que anotan, por ejemplo:

      @MiAnotación(nombre = "unNombre", valor = "unValor")
      public class MiClase{ }

   Si sólo tiene un parámetro, puede escribirse así: 

      @MiAnotación("unNombre")

   La forma de definir una anotación es como la de una interfaz con el signo @ delante:

      public @ interface MiAnotación{
         String nombre(); //miembros declarados como métodos sin argumentos
         String valor();
      }

   Una vez que has definido una anotación, ya puedes usarla en tu código. Por ejemplo, puedes crear una anotación para especificar los autores y versiones de un determinado programa:

      
@interface Creación{
      String autores();
      String fecha();
      int versión() default 1;
}

  Y ahora, al inicio del programa:

@Creación(  //con paréntesis
          autores = "Java Para Nulos", //parámentros separados por comas
          fecha = "05/04/2012",
          versión = 3 //si no se indicase nada, se aplicaría el valor por defecto
         )

  Hay algunas anotaciones ya predefinidas en el lenguaje y que resultan útiles a la hora de compilar:


     @Override - informa al compilador que el método al que anota está sobreescribiendo un método de la superclase. Si por algún motivo no sobreescribimos bien el método, el compilador genera un error indicando dónde está el fallo.


    @Deprecated - un elemento marcado con esta anotación indica que está en desuso, bien porque es peligroso, bien porque hay otra alternativa mejor. Un elemento así debería ser también comentado utilizando la etiqueta @deprecated(con minúscula). El compilador genera un aviso cuando se utiliza un elemento marcado @Deprecated.


    @SuppressWarnings - indica al compilador que elimine dos tipos de advertencias que generaría de otro modo: 

  • deprecation: para que no genere una advertencia cuando se utiliza un elemento marcado como @Deprecated.
  • unchecked:  cuando se utiliza código creado antes de la aparición de los genéricos ( y no hablamos de medicamentos aquí).
   La forma de escribir esta anotación es la siguiente:
      @SuppressWarnings("deprecation")
      o en caso de que sean los dos tipos:
      @SuppressWarnings({"unchecked", "deprecation"})


   La anotación @SuppressWarnings debe utilizarse con precaución y aplicándola al mínimo elemento posible. Es decir, si no quieres que el compilador genere una advertencia por utilizar un método en desuso, marca ese método, y no toda la clase, porque podrías eliminar otras advertencias importantes.


   Esto nos lleva a las Meta-Anotaciones, o anotaciones de anotaciones. Por ejemplo, si marcamos una anotación con @Target(ElementType.METHOD) estamos indicando que dicha anotación sólo se aplica al método. ElementType es un enum, y los demás valores que tiene son:

  • FIELD
  • PARAMETER
  • CONSTRUCTOR
  • LOCAL_VARIABLE
  • ANNOTATION_TYPE
  • PACKAGE
   Además de @Target, tenemos las siguientes meta-anotaciones:


   @Retention - que indica hasta dónde se mantienen las anotaciones. Hay tres modos posibles:

  • Retention(RetentionPolicy.SOURCE), la anotación sólo se aplica al código fuente, siendo ignorada por el compilador y la JVM.
  • Retention(RetentionPolicy.CLASS), el compilador puede ver la anotación y actuar en consecuencia, pero es ignorada por la JVM.
  • Retention(RetentionPolicy.RUNTIME), la JVM puede ver la anotación y utilizarla en tiempo de ejecución con Reflection.

   @Documented - indica que la anotación deberá ser tenida en cuenta por la herramienta javadoc o similares. Estas anotaciones pasan a ser parte del API público de los elementos anotados. Es un tipo de anotación "marcador", es decir, no tiene miembros.



   @Inherited - indica que una anotación es automáticamente heredada. Esta meta-anotación sólo tiene efecto en anotaciones de clase, y sólo se hereda de superclases, no de interfaces.


   Cualquier duda o comentario que queráis hacer, ya sabéis dónde estamos ; )