Lugar para compartir información interesante con mis amigos.

Tuesday, April 14, 2015

Tutorial de concurrencia en Java 7, Parte 1


Esta es una entrada resultado de la traducción el tutorial para Concurrencia publicado en la página oficial de Oracle.

Los usuarios de computador dan por sentado que sus sistemas pueden hacer más que una cosa a la vez. Ellos asumen que pueden continuar trabajando en el procesador de palabras, mientras otras aplicaciones descargan archivos, administran la cola de impresión, y hacen streaming de audio. Incluso una sola aplicación se espera aveces que haga más que una cosa a la vez. Por ejemplo, una aplicación de streaming de audio debe simultáneamente leer el audio digital de la red, descomprimirlo, administrar la reproducción, y actualizar la visualización. Incluso el procesador de palabras debería siempre estar listo para responder a los eventos del teclado y del ratón, sin importar qué tan ocupado esté formateando texto o actualizando la visualización. El software que puede hacer esas cosas es conocido como software concurrente.




La plataforma Java está diseñada desde la base para soportar programación concurrente, con soporte para concurrencia básico a nivel del lenguaje de programación y de librerías de clases. Desde la versión 5.0, la plataforma java también ha incluido API’s de concurrencia de alto nivel. Esta lección es una introducción al soporte básico de plataforma para concurrencia y resume algunas de las APIs de alto nivel en los paquetes java.util.concurrent.


Procesos e Hilos



En programación concurrente, hay dos unidades básicas de ejecución: procesos e hilos. En el lenguaje de programación Java, la programación concurrente se trata sobre todo de hilos. Sin embargo, los procesos también son importantes.


Un sistema computacional normalmente tiene muchos procesos e hilos activos. Esto es verdad incluso en sistemas que solo tienen un único núcleo, y así solo tiene un hilo en ejecución en un momento dado. El tiempo de procesamiento para un único núcleo es compartido entre procesos e hilos a través de un a característica del SO llamada segmentación de tiempo (time slicing).


Se está volviendo más y más común para los sistemas computacionales tener múltiples procesadores o procesadores con múltiples núcleos de ejecución. Esto realmente mejora la capacidad del sistema para ejecución concurrente de procesos e hilos -- pero la concurrencia es posible incluso en sistemas simples, sin múltiples procesadores o núcleos.


Procesos

Un proceso tiene un entorno de ejecución antocontenido. Un proceso generalmente tiene un completo y privado conjunto de recursos de tiempo de ejecución; en particular, cada proceso tiene su propio espacio de memoria.


Los procesos son a veces vistos como sinónimos de programas o aplicaciones. Sin embargo, lo que el usuario ve como una aplicación individual puede de hecho ser un conjunto de procesos cooperando. Para facilitar la comunicación entre procesos la mayoría de SO da soporte a recursos Inter Process Communication (IPC), como las tuberías (pipes) y sockets. IPC es usado no solo para comunicación entre procesos en el mismo sistema, sino también procesos en diferentes sistemas.
La mayoría de implementaciones de la máquina virtual de Java corre como un único proceso. una plicación Java puede crear procesos adicionales utilizando un objeto ProcessBuilder. Aplicaciones multiproceso están más allá del alcance de esta lección.


Hilos

Los hilos son a veces llamados procesos livianos. Ambos, procesos a hilos proveen un entorno de ejecución, pero crear un nuevo hilo requiere menos recursos que crear un nuevo proceso.
Los hilos existen dentro de un proceso -- cada proceso tiene al menos uno. Hilos comparten los recursos del proceso, incluyendo memoria y archivos abiertos. Esto se hace para una comunicación eficiente, pero  potencialmente problemática.


Ejecución multihilo es una característica esencial de la plataforma Java. Cada aplicación tiene al menos un hilo -- o muchos, si contamos los hilos de sistema que hacen cosas como administración de memoria y manejo de señales. Pero desde el punto de vista del programador, usted solo inicia con solo un hilo, llamado el hilo main. Este hilo tiene la habilidad de crear hilos adicionales como vamos a mostrar en la siguiente sección.


Objetos Thread

Cada hilo está asociado con una instancia de la clase Thread. Hay dos estratégias básicas para usar objetos Thread para crear una aplicación concurrente.
  • Directamente controlar la creación y administración del hilo, simplemente instanciar Thread cada vez que la aplicación necesita inicializar una tarea asíncrona.
  • Abstraer la administración de hilos del resto de la aplicación, pasar una aplicación de tareas a un ejecutor.
Esta sección documenta el uso de objetos Thread. Ejecutores son discutidos con otros objetos de alto nivel de concurrencia.


Definiendo e iniciando un hilo

Una aplicación que crea e instancia Threads debe proveer el código que va a correr en ese hilo. Hay dos maneras de hacer eso:
  • Crear un objeto Runnable. La interface Runnable define un sólo método, run, que contiene el código ejecutado por el hilo. El objeto Runnable es pasado al constructor del Thread, como el el ejemplo HelloRunnable:
public class HelloRunnable implements Runnable {

   public void run() {
       System.out.println("Hello from a thread!");
   }

   public static void main(String args[]) {
       (new Thread(new HelloRunnable())).start();
   }

}


  • Heredar de Thread. La clase Thread misma implementa Runnable, pero su método run no hace nada. Una aplicación puede heredar de Thread, anexando su propia implementación de run, como en el ejemplo HelloThread:


public class HelloThread extends Thread {

   public void run() {
       System.out.println("Hello from a thread!");
   }

   public static void main(String args[]) {
       (new HelloThread()).start();
   }

}
Note que los dos ejemplos invocan el método Thread.start para iniciar el nuevo hilo.


Cuáles de estos dos enfoques debería usar? El primero, que emplea el objeto Runnable, es más general, porque el objeto Runnable puede heredar otra clase distinta a Thread. El segundo enfoque es más fácil de usar en aplicaciones simples, pero es limitado por el hecho que su clase que implementa la tarea debe heredar de Thread. Esta lección se enfoca en el primer acercamiento, el cual separa la tarea Runnable del objeto Thread que ejecuta la tarea. Este acercamiento no solo es más flexible, sino que es aplicable a la API de administración de hilos que veremos después.


La clase Thread define un número de métodos útiles para la administración de hilos. Estos incluyen métodos estáticos, que proveen información acerca, o afectan el estado de, el hilo que invoca el método. Los otros métodos son invocados desde otros hilos involucrados en la administración del hilo y del objeto Thread. Vamos a examinar algunos de estos métodos en las siguientes secciones.


Pausando la ejecución con Sleep

Thread.sleep causa que el hilo actual suspenda la ejecución por un periodo específico de tiempo. Esta es una manera eficiente de obtener tiempo de procesamiento disponible para otros hilos de una aplicación o para otras aplicaciones que pueden estar corriendo en el computador. El método sleep puede también ser usado para estimular, como se muestra en el siguiente ejemplo, y para esperar por otro hilo con deberes para los cuales tiene requerimientos de tiempo, como con el ejemplo SimpleThreads de la sección anterior.
Son facilitadas dos versiones sobrecargadas de sleep: una que especifica el tiempo de reposo en milisegundos y otro que especifica el tiempo de reposo en nanosegundos. Sin embargo, estos tiempos de reposo no son garantizados para ser precisos, porque están limitados por la infraestructura proveída por el SO subyacente. También, el periodo de reposo puede ser terminado por interrupciones, como veremos en más tarde. En cualquier caso, usted no puede asumir que invocar el método sleep va a suspender el hilo precisamente por el tiempo especificado.


El ejemplo SleepMessages usa sleep para imprimir mensajes con intervalos de cuatro segundos.
public class SleepMessages {
   public static void main(String args[])
       throws InterruptedException {
       String importantInfo[] = {
           "Mares eat oats",
           "Does eat oats",
           "Little lambs eat ivy",
           "A kid will eat ivy too"
       };

       for (int i = 0;
            i < importantInfo.length;
            i++) {
           //Pause for 4 seconds
           Thread.sleep(4000);
           //Print a message
           System.out.println(importantInfo[i]);
       }
   }
}
Note que main declara throws InterruptedException. Esta es una excepción que lanza sleep cuando otro hilo interrumpe el hilo actual mientras sleep está activo. Debido a que esta aplicación no define otro hilo que cause la interrupción, no se preocupe por atrapar InterruptedException.


Interrupciones

Una interrupción es una indicación al hilo que debería parar lo que está haciendo y hacer otra cosa. Es responsabilidad del programador decidir exactamente cómo el hilo responde a la interrupción, pero es común para el hilo terminar. Este es el énfasis en esta lección.


Un hilo envía una interrupción invocando el método interrupt en el objeto Thread del hilo a ser interrumpido.  Para que el mecanismo de interrupción trabaje correctamente, el hilo interrumpido debe soportar su propia interrupción.

Soportando Interrupciones

Cómo un hilo soporta su propia interrupción? Esto depende de qué está actualmente haciendo. Si el hilo  frecuentemente invoca métodos que lanzan InterruptedException, éste simplemente retorna desde el método run después que éste atrapa la excepción. Por ejemplo, suponga que el bucle central de mensajes en el ejemplo SleepMessages  estuviera en el método run de un hilo de un objeto Runnable. Entonces éste puede ser modificado como sigue para soportar interrupciones.


for (int i = 0; i < importantInfo.length; i++) {
   // Pause for 4 seconds
   try {
       Thread.sleep(4000);
   } catch (InterruptedException e) {
       // We've been interrupted: no more messages.
       return;
   }
   // Print a message
   System.out.println(importantInfo[i]);
}
Muchos métodos que lanzan InterruptedException como sleep, están diseñados para cancelar su operación actual y retornar inmediatamente cuando una interrupción es recibida.
Qué pasa si un hilo pasa el tiempo sin invocar un método que lance un método que lance InterruptedExceptión? Entonces debe periódicamente inocar Thread.interrupted, el cual retorna true si una interrupción ha sido recibida. Por ejemplo:
for (int i = 0; i < inputs.length; i++) {
   heavyCrunch(inputs[i]);
   if (Thread.interrupted()) {
       // We've been interrupted: no more crunching.
       return;
   }
}
En este ejemplo simple, el código simplemente pregunta por la interrupción y sale del hilo si una ha sido recibida. En aplicaciones más complejas, pudo tener más sentido lanzar una InterruptedException.


if (Thread.interrupted()) {
   throw new InterruptedException();
}


Esto permite que el código de manejo de interrupciones sea centralizado en la centencia catch.

La bandera estado de interrupción.

El mecanismo de interrupción está implementado usando una bandera interna conocida como el estado de interrupción (interrupt status). Invocando Thread.interrupt establece ésta bandera. Cuando un hilo verifica por una interrupción invocando el método estático Thread.interrupted, el estatus de interrupción es limpiado. El método no estático isInterrupted, el cual es usado por un hilo para preguntar el estado de interrupción de otro, no cambia el estado de la bandera.


Por convención, cualquier método que termina lanzando un InterruptedException limpia el estado de interrupción cuando hace eso. Sin embargo, siempre es posible que el estado de interrupción pueda inmediatamente ser puesto de nuevo, por otro hilo invocando interrupt.

Joins

El método join permite a un hilo esperar por la terminación de otro. Si t es un objeto Thread cuyo hilo está actualmente ejecutándose,


t.join();


causa que el hilo actual se pause en su ejecución hasta que el hilo t termine. Sobrecargas de join permiten al programador especificar un periodo de espera. Sin embargo como con sleep, join es dependiente del sistema operativo para el tiempo, así que usted no debería asumir que join va a esperar exactamente el tiempo que usted especifica.


Como sleep, join responde a una interrupción saliendo con una InterruptedException.


Ejemplo simple de hilos

El siguiente ejemplo trae junto algunos conceptos de esta sección. SimpleThreads está formado por dos hilos. El primero es el hilo main que cada aplicación Java tiene. El hilo main crea un nuevo hilo del objeto Runnable, MessageLoop, y espera para su terminación. Si el hilo MessageLoop toma mucho tiempo en terminar, el hilo main lo interrumpe.
El hilo MessageLoop imprime una serie de mensajes. Si es interrumpido antes que haya impreso todos sus mensajes, el hilo MessageLoop imprime un mensaje y termina.


public class SimpleThreads {

   // Display a message, preceded by
   // the name of the current thread
   static void threadMessage(String message) {
       String threadName =
           Thread.currentThread().getName();
       System.out.format("%s: %s%n",
                         threadName,
                         message);
   }

   private static class MessageLoop
       implements Runnable {
       public void run() {
           String importantInfo[] = {
               "Mares eat oats",
               "Does eat oats",
               "Little lambs eat ivy",
               "A kid will eat ivy too"
           };
           try {
               for (int i = 0;
                    i < importantInfo.length;
                    i++) {
                   // Pause for 4 seconds
                   Thread.sleep(4000);
                   // Print a message
                   threadMessage(importantInfo[i]);
               }
           } catch (InterruptedException e) {
               threadMessage("I wasn't done!");
           }
       }
   }

   public static void main(String args[])
       throws InterruptedException {

       // Delay, in milliseconds before
       // we interrupt MessageLoop
       // thread (default one hour).
       long patience = 1000 * 60 * 60;

       // If command line argument
       // present, gives patience
       // in seconds.
       if (args.length > 0) {
           try {
               patience = Long.parseLong(args[0]) * 1000;
           } catch (NumberFormatException e) {
               System.err.println("Argument must be an integer.");
               System.exit(1);
           }
       }

       threadMessage("Starting MessageLoop thread");
       long startTime = System.currentTimeMillis();
       Thread t = new Thread(new MessageLoop());
       t.start();

       threadMessage("Waiting for MessageLoop thread to finish");
       // loop until MessageLoop
       // thread exits
       while (t.isAlive()) {
           threadMessage("Still waiting...");
           // Wait maximum of 1 second
           // for MessageLoop thread
           // to finish.
           t.join(1000);
           if (((System.currentTimeMillis() - startTime) > patience)
                 && t.isAlive()) {
               threadMessage("Tired of waiting!");
               t.interrupt();
               // Shouldn't be long now
               // -- wait indefinitely
               t.join();
           }
       }
       threadMessage("Finally!");
   }
}

Sincronización.

Los hilos se comunican principalmente compartiendo el acceso a campos y los objetos a los cuales esos campos se refieren. Esta forma de comunicación es extremadamente eficiente, pero crea dos tipos posibles de error: interferencia entre hilos y errores de inconsistencia de memoria. La herramienta necesaria para prevenir estos errores es la sincronización.
Sin embargo, la sincronización puede agregar contención de hilos, la cual ocurre cuando dos o más hilos intentan acceder el mismo recurso simultáneamente y causar que la máquina virtual de Java ejecute uno o más hilos más lentamente, o incluso suspender su ejecución.
Esta sección cubre los siguientes tópicos:
  • Interferencia entre Hilos, describe cómo son introducidos errores cuando muchos hilos acceden datos compartidos.
  • Errores de consistencia de memoria, describe errores que resultan de la lectura inconsistente de memoria compartida.
  • bloqueo y sincronización implícita, describe una forma más general de sincronización y describe cómo la sincronización está basada en bloqueos implícitos.
  • Acceso atómico, habla acerca de la idea general operaciones que no deben ser interrumpidas por otros hilos.


Interferencia entre hilos

Considere la simple clase llamada Counter.
class Counter {
   private int c = 0;

   public void increment() {
       c++;
   }

   public void decrement() {
       c--;
   }

   public int value() {
       return c;
   }

}


Counter está diseñada de tal forma que cada invocación de increment va a sumar 1 a c, y cada invocación de decrement va a restar 1 de c, Sin embargo, si un objeto Counter es referenciado por múltiples hilos, la interferencia entre hilos puede evitar que las cosas sucedan como se espera.
La interferencia sucede cuando dos operaciones, corriendo en hilos diferentes, pero actuando sobre los mismos datos, se entrelazan . Esto significa que dos operaciones consistentes de múltiples pasos, y la secuencia de pasos se sobreponen.


No parecería ser posible que operaciones sobre instancias de Counter se intercalen debido a que ambas operaciones son sentencias individuales y simples. Sin embargo, incluso sentencias simples pueden traducirse a múltiples pasos por la máquina virtual. Nosotros no examinamos los pasos específicos de la máquina virtual -- es suficiente saber que la simple expresión c++ puede ser descompuesta en tres pasos:


  1. Obtener el valor actual de c.
  2. incrementar el valor recuperado en 1.
  3. Almacenar el valor incrementado de vuelta en c.


La expresión c-- puede ser descompuesta en la misma manera, excepto que el segundo paso decrementa en lugar de incrementar.


Suponga que el Hilo A invoca increment y casi al mismo tiempo el Hilo B invoca decrement. Si el valor inicial de c es 0, sus acciones intercaladas pueden seguir la siguiente secuencia:


  1. Hilo A: Recupera c.
  2. Hilo B: Recupera c.
  3. Hilo A: Incrementa el valor recuperado; resultado es 1.
  4. Hilo B Decrementa el valor recuperado; resultado es -1.
  5. Hilo A: Almacena el valor en c; c es ahora 1.
  6. Hilo B: Almacena el valor en c; c es ahora -1.
El resultado del hilo A se pierde, es sobreescrito por el hilo B. Este intercalado en particular es solo una posibilidad. Bajo diferentes circunstancias puede ser el resultado del hilo B el que se pierda o puede que no haya errores del todo. Porque ellos son impredecibles, los errores de interferencia entre hilos pueden ser difíciles de detectar y solucionar.


Errores de consistencia de memoria

Errores de consistencia de memoria ocurren cuando diferentes hilos tienen lecturas inconsistentes de lo que debería ser el mismo dato. Las causas de los errores de inconsistencia de memoria son complejos y van más allá del alcance de este tutorial. Afortunadamente, el programador no necesita un entendimiento detallado de estas causas. Todo lo que necesita es una estrategia para evitarlos.


La clave para evitar los errores de consistencia de memoria es entender la relación de lo que sucede-antes. Esta relación es simplemente una garantía que la memoria escriba por una sentencia específica es visible por otra sentencia específica. Para ver esto, considere el siguiente ejemplo. Suponga un simple campo int definido e inicializado:


int counter = 0;


el campo counter es compartido entre dos hilos, A y B. Suponga que el hilo A incrementa counter.


counter ++;


Entonces enseguida el hilo B imprime counter:


System.out.println (counter);


Si las dos sentencias han sido ejecutado en el mismo hilo, sería seguro asumir que el valor impreso podría ser “1”. Pero si las dos sentencias son ejecutadas en hilos separados, el valor impreso podría ser “0”, porque no hay garantía que el cambio hecho por el hilo A a counter va a ser visible al hilo B -- amenos que el programador haya establecido una relación sucede-antes entre estas dos sentencias.


Hay muchas acciones que crean relaciones sucede-antes. Una de ellas es la sincronización, como vamos a ver en las siguientes secciones:


Nosotros ya hemos visto dos acciones para crear relaciones sucede-antes.


  • Cuando una sentencia invoca Thread.start, cada sentencia que tiene una relación sucedido-antes con esa sentencia también tiene una relación sucedió-despues con cada sentencia ejecutada por el nuevo hilo. Los efectos del código que condujo a la creación del nuevo hilo son visibles para el nuevo hilo.
  • Cuando un hilo termina y causa un Thread.join in otro hilo para retornar, entonces todas las sentencias ejecutadas por el hilo terminado tienen una relación sucedió-antes con todas las sentencias que suceden el join exitoso. Los efectos del código en el hilo son ahora visibles para el hilo que ejecuta el join.


Para una lista de acciones que crean relaciones sucede-antes, vaya a la página resumen del paquete java.util.concurrent.


Métodos sincronizados

El lenguaje de programación Java ofrece dos mecanismos para sincronización: métodos sincronizados y sentencias sincronizadas. El más complejo de los dos, sentencias sincronizadas, es descrito en la siguiente sección. Esta sección trata acerca de métodos sincronizados.


Para hacer un método sincronizado simplemente agregue la palabra clave sinchronized a su declaración:


public class SynchronizedCounter {
   private int c = 0;

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

   public synchronized void decrement() {
       c--;
   }

   public synchronized int value() {
       return c;
   }
}
Si count es una instancia de SyncronizedCounter, entonces hacer estos métodos sincronizados tiene dos efectos:


  • Primero, no es posible que para dos invocaciones de métodos sincronizados en el mismo objeto que se intercalen. Cuando un hilo está ejecutando un método sincronizado para un objeto, todos los otros hilos que invocan métodos sincronizados para el mismo objeto se bloquean (suspenden la ejecución) hasta que el primero hilo termine con el objeto.
  • Segundo, cuando un método sincronizado termina, este automáticamente establece una relación sucede-antes con cualquier invocación subsecuente de un método sincronizado para el mismo objeto. Esto garantiza que cambios al estado del objeto son visibles para todos los hilos.


Tenga en cuenta que los constructores no pueden ser sincronizados -- usar la palabra clave synchronized con un constructor genera un error de sintaxis. Sincronizar constructores no tiene sentido, porque solo el hilo que crea un objeto podría tener acceso a él mientras éste es construido.




Advertencia: Cuando se construye un objeto que va a ser compartido entre hilos, sea muy cuidadoso que la referencia al objeto no se pierda prematuramente. Por ejempo, suponga que usted quiere mantener un List llamado instancias conteniendo cada instancia de clase. Usted puede ser tantado a adicionar la siguiente línea a su constructor.


instances.add(this);


pero entonces otros hilos pueden usar instances para acceder al objeto antes que la construcción del objeto esté completa.




Métodos sincronizados permiten una estrategia simple para evitar errores de consistencia de memoria e interferencia de hilos: si un objeto es visible para más de un hilo, todos los hilos que leen o escriben a las variables del objeto lo hacen a través de métodos sincronizados. (Una importante excepción: campos final, los cuales no pueden ser modificado después que el objeto es construido, pueden ser leídos con seguridad a través de métodos no sincronizados, una vez que el objeto es construido). Esta estrategia es efectiva, pero puede presentar problemas con la vivencia, como veremos después en esta lección.


Bloqueo y sincronización intrínsecos

Sincronización está construida alrededor de una entidad interna conocida como el seguro intrínseco o monitor de bloqueo. (La especificación API a veces se refiere a esta entidad simplemente como “monitor” ) Bloqueos intrínsecos juegan un rol en ambos aspectos de sincronización: obligando acceso exclusivo a estados de un objeto y estableciendo relaciones sucede-antes que son esenciales para la visibilidad.


Cada objeto tiene un seguro intrínseco con él. Por convención, un hilo que necesita acceso exclusivo y consistente a los campos de un objeto tiene que adquirir el seguro intrínseco del objeto antes de acceder a él, y entonces liberar el seguro intrínseco cuando haya terminado con él. Un hilo se dice que mantiene el seguro intrínseco entre el momento en que este es adquirido y en el momento en que es liberado. Tan pronto como el hilo se apodera de un seguro intrínseco, ningún otro hilo puede adquirir el mismo seguro. El otro hilo se bloqueará cuando intente obtener el bloqueo.


Cuando un hilo libera un seguro intrínseco, una relación sucede-antes es establecida entre esa acción y cualquier obtención subsecuente del mismo bloqueo.


Bloqueos en métodos sincronizados

Cuando un hilo invoca un método sincronizado, este automáticamente obtiene el bloqueo intrínseco para ese método del objeto y lo libera cuando el método retorna. La liberación del bloqueo ocurre incluso si el retorno es causado por una excepción en tiempo de ejecución.


Usted puede sorprenderse de lo que sucede cuando un método sincronizado estático es invocado, puesto que un método estático está asociado con la clase y no con un objeto. En éste caso, el hilo adquiere el bloqueo intrínseco para el objeto Class asociado con la clase. Así que el acceso a los campos estáticos son controlados por un seguro que es distinto del seguro de cualquier instancia de la clase.


Bloques sincronizados

Otra forma para crear código sincronizado es con bloques sincronizados. A diferencia de los métodos sincronizados, los bloques sincronizados deben especificar el objeto que provee el bloqueo intrínseco:


public void addName(String name) {
   synchronized(this) {
       lastName = name;
       nameCount++;
   }
   nameList.add(name);
}


En este ejemplo, el método addName necesita sincronizar los cambios a las variables lastName y nameCount, pero también necesita evitar invocaciones sincronizadas de los otros métodos del objeto. (invocar otros métodos del objeto desde código sincronizado puede crear problemas que son descritos en la sección Liveness) Sin bloques sincronizados, debería haber un método sincronizado separado con el único propósito de invocar nameList.add.


Bloques sincronizados son también útiles para mejorar la concurrencia a un nivel granulado de sincronización. Suponga, por ejemplo, la clase MsLunch que tiene dos atributos de instancia, c1 y c2, que nunca son usados juntos. Todas las actualizaciones a estos atributos deben ser sincronizadas, pero no hay razón para evitar una actualización de c1 de ser intercalada con una actualización de c2 -- y haciendo eso reduce la concurrencia creando un bloqueo innecesario. En lugar de usar métodos sincronizados o a cambio de usar bloqueos asociados con this, nosotros creamos dos objetos únicamente para obtener los bloqueos.


public class MsLunch {
   private long c1 = 0;
   private long c2 = 0;
   private Object lock1 = new Object();
   private Object lock2 = new Object();

   public void inc1() {
       synchronized(lock1) {
           c1++;
       }
   }

   public void inc2() {
       synchronized(lock2) {
           c2++;
       }
   }
}
Use esta característica con extremo cuidado. Usted debe estar absolutamente seguro que es realmente seguro intercalar acceso de los atributos en discusión.


Sincronización Reentrante

Recordemos que un hilo no puede adquirir el bloqueo que tiene otro hilo. Pero un hilo puede obtener un bloqueo que él ya tiene. Permitiendo al hilo adquirir el mismo bloqueo más de una vez permite la sincronización reentrante. Éste describe una situación donde el código sincronizado , directa o indirectamente, invoca un método que también contiene código sincronizado, y ambos conjuntos de código usan el mismo bloqueo. Sin sincronización reentrante, el código sincronizado tendría que tomar muchas precauciones adicionales para evitar que un hilo se bloquee a sí mismo.


Acceso atómico.

En programación, una acción atómica es una que efectivamente sucede toda de una sola vez. Una acción atómica no puede parar en la mitad: también sucede completamente, o simplemente no sucede. No hay efectos colaterales visibles de una acción atómica hasta que la acción está completa.
Nosotros ya hemos visto la expresión de incremento, que es c++, no es una acción atómica. Incluso expresiones muy simples pueden definir acciones complejas que se pueden descomponer en otras acciones. Sin embargo, hay acciones que se pueden especificar que son atómicas:


  • Lecturas y escrituras son atómicas para variables de objetos y para la mayoría de variables primitivas (todas excepto long y double).
  • Lecturas y escrituras son atómicas para todas las variables declaradas volatile (incluidas las variables long y double.).


Acciones atómicas no pueden ser intercaladas, así que ellas pueden ser usadas sin temor a interferencia entre hilos. Sin embargo, esto no elimina toda la necesidad de sincronizar acciones atómicas, porque aún es posible que se causen  los errores de consistencia de memoria. Usar variables volátiles reduce el riesgo de errores de consistencia de memoria, porque cualquier escritura a una variable volatile establece una relación sucede-antes con subsecuentes lecturas de la misma variable. Esto significa que cambios a la variable volatile son siempre visibles a los otros hilos. Lo que es más, esto también significa que cuando un hilo lee una variable volátil, éste ve no sólo el último cambio hecho al volatile, sino también los efectos secundarios del código que llevaron el cambio.
Usando un simple acceso atómico a una variable es más eficiente que accesar estas variables a través de código sincronizado, pero requiere más cuidado por el programador para evitar errores de consistencia de memoria. Si el esfuerzo adicional vale la pena, depende del tamaño y complejidad de la aplicación.


Algunas de las clases en el paquete java.util.concurrent ofrecen métodos atómicos que no usan sincronización. Vamos a discutirlos en la sección de Objetos de concurrencia de alto nivel.


Vivacidad

La habilidad de una aplicación concurrente para ejecutar de una manera ordenada es conocida como vivacidad (Liveness). Esta sección describe la forma más común de problemas con la vivacidad, el bloqueo mortal (deadlock), y brevemente describe otros dos problemas de vivacidad, starvation (hambruna) y livelock (bloqueo vivo).


Bloqueo mortal

Bloqueo mortal describe una situación donde dos o más hilos están bloqueados para siempre, esperando uno al otro entre sí. Aquí hay un ejemplo.


Alfonso y Gastón son amigos, y grandes creyentes en la cortesía. Una regla estricta de cortesía es que cuando te inclinas a un amigo, debes mantenerte inclinado hasta que tu amigo tenga la oportunidad para retornar la inclinación. Desafortunadamente, esta regla no da espacio a la posibilidad que dos amigos puedan hacer la inclinación el uno al otro al mismo tiempo. Esta aplicación de ejemplo, Deadlock, modela esta posibilidad.


public class Deadlock {
   static class Friend {
       private final String name;
       public Friend(String name) {
           this.name = name;
       }
       public String getName() {
           return this.name;
       }
       public synchronized void bow(Friend bower) {
           System.out.format("%s: %s"
               + "  has bowed to me!%n",
               this.name, bower.getName());
           bower.bowBack(this);
       }
       public synchronized void bowBack(Friend bower) {
           System.out.format("%s: %s"
               + " has bowed back to me!%n",
               this.name, bower.getName());
       }
   }

   public static void main(String[] args) {
       final Friend alphonse =
           new Friend("Alphonse");
       final Friend gaston =
           new Friend("Gaston");
       new Thread(new Runnable() {
           public void run() { alphonse.bow(gaston); }
       }).start();
       new Thread(new Runnable() {
           public void run() { gaston.bow(alphonse); }
       }).start();
   }
}
Cuando Deadlock corre, es extremadamente posbile que ambos hilos se bloqueen cuando ellos intenten invocar bowBack. Ningún bloqueo va a terminar nunca, porque cada hilo está esperando porque el otro bloqueo termine.


Starvation and Livelock

Starvation y livelock son problemas mucho menos comunes que el abrazo mortal, pero aún son problemas que cada diseñador de software concurrente puede encontrar.


Starvation

Describe una situación donde un hilo no puede ganar acceso regular a un recurso compartido y le es imposible avanzar. Esto sucede cuando recursos compartidos no están disponibles por largos periodos de tiempo debido a hilos “greedy” golosos. Por ejemplo, suponga que un objeto ofrece un método sincronizado que a veces demora en retornar. Si un hilo invoca este método frecuentemente, otros hilos que también necesiten frecuente acceso sincronizado al mismo objeto se van a bloquear a veces.


Livelock

Un hilo a veces actúa en respuesta a la acción de otro hilo. Si la acción del otro hilo es también una respuesta a la acción de otro hilo distinto, entonces puede resultar un livelock. Como el deadlock, hilos livelock no pueden seguir avanzando. Sin embargo, los hilos no están bloqueados -- ellos simplemente están muy ocupados respondiendo el uno al otro la continuación del trabajo. Esto es comparable a dos personas intentando pasar una a la otra en un corredor: Alfonso mueve a la izquierda para permitir a Gastón pasar, mientras Gastón se mueve a la derecha para permitir a Alfonso pasar. Viendo que ellos están aún bloqueándose el uno al otro, Alfonso se mueve a su derecha, mientras Gastón se mueve a su izquierda. Ellos están aún bloqueandose entre sí, así que…..


Bloques vigilados

Los hilos a veces tienen que coordinar sus acciones. El estilo de coordinación más común son los bloques vigilados o guarded block. El cual es un bloque que comienza jalando de una condición que debe ser verdadera antes que el bloque pueda continuar. Hay un número de pasos para seguir para que se haga correctamente.


Suponga, por ejemplo el método guardedJoy que no debe iniciar hasta que la variable compartida join ha sido establecida por otro hilo. Ese método podría, en teoría, simplemente iterar hasta que la condición sea satisfecha pero esa iteración es un desperdicio, ya que esta se ejecuta continuamente mientras espera.


public void guardedJoy() {
   // Simple loop guard. Wastes
   // processor time. Don't do this!
   while(!joy) {}
   System.out.println("Joy has been achieved!");
}
Una forma más eficiente de guarda invoca Object.wait para suspender el hilo actual. La invocación de wait no retorna hasta que otro hilo ha enviado una notificación que un evento especial pudo haber ocurrido -- así que no necesariamente el evento que el hilo está esperando.


public synchronized void guardedJoy() {
   // This guard only loops once for each special event, which may not
   // be the event we're waiting for.
   while(!joy) {
       try {
           wait();
       } catch (InterruptedException e) {}
   }
   System.out.println("Joy and efficiency have been achieved!");
}


Nota: Siempre que invoque wait dentro de un bucle que revisa por una condición por la cual está esperando. No asuma que la interrupción fue por una condición en particular por la cual está esperando, o que la condición está aún en true.


Como muchos métodos que suspenden la ejecución, wait puede lanzar InterruptedException. En este ejemplo, podemos ignorar esa excepción -- nosotros solo vigilamos el valor de joy.


Por qué está sincronizada esta versión de guardedJoy? Suponga que d es el objeto que estamos usando para invocar wait. cuando un hilo invoca d.wait, este debe obtener el bloqueo intrínseco para d -- de otro modo un se lanza un error. Invocando wait dentro de un método sincronizado es una manera simple para obtener el bloqueo intrínseco.


Cuando wait es invocado, el hilo libera el bloqueo y suspende la ejecución. En algún momento futuro, otro hilo va a obtener el mismo bloqueo e invocar Object.notifyAll, informando a todos los hilos que están esperando sobre ese bloqueo que algo importante acaba de pasar:


public synchronized notifyJoy() {
   joy = true;
   notifyAll();
}


Algún tiempo después que el segundo hilo ha liberado el bloqueo, el primer hilo vuelve a adquirir el bloqueo y vuelve retomando desde la invocación del wait.




Nota: hay un segundo método de notificación, notify, wl cual despierta un único hilo. Porque notify no permite especificar el hilo a despertar, es útil sólo en aplicaciones masivamente paralelas -- esto es, programas con un número grande de hilos, todos haciendo tareas similares. En dicha aplicación no importa cuál hilo ha desperado.




Vamos a usar bloques vigilados para crear una aplicación Productor-Consumidor. Este tipo de aplicación comparte datos entre dos hilos: el productor, que crea los datos, y el consumidor, que hace algo con ellos. Los dos hilos se comunican usando un objeto compartido. Coordinación es esencial: el hilo consumidor no debe intentar obtener datos antes que el hilo productor lo ha entregado, y el hilo productor no debe intentar entrega de nuevos datos si el consumidor no ha retirado el dato antiguo.


En este ejemplo, los datos son una serie de mensajes de texto, los cuales son compartidos a través de un objeto de tipo Drop


public class Drop {
   // Message sent from producer
   // to consumer.
   private String message;
   // True if consumer should wait
   // for producer to send message,
   // false if producer should wait for
   // consumer to retrieve message.
   private boolean empty = true;

   public synchronized String take() {
       // Wait until message is
       // available.
       while (empty) {
           try {
               wait();
           } catch (InterruptedException e) {}
       }
       // Toggle status.
       empty = true;
       // Notify producer that
       // status has changed.
       notifyAll();
       return message;
   }

   public synchronized void put(String message) {
       // Wait until message has
       // been retrieved.
       while (!empty) {
           try {
               wait();
           } catch (InterruptedException e) {}
       }
       // Toggle status.
       empty = false;
       // Store message.
       this.message = message;
       // Notify consumer that status
       // has changed.
       notifyAll();
   }
}
El hilo productor, definido en Producer, envía conjuntos de mensajes familiares. La cadena “DONE” indica que todos los mensajes han sido enviados. Para simular la naturaleza impredecible de las aplicaciones del mundo real, el hilo productor pausa a intervalos aleatorios entre mensajes.


import java.util.Random;

public class Producer implements Runnable {
   private Drop drop;

   public Producer(Drop drop) {
       this.drop = drop;
   }

   public void run() {
       String importantInfo[] = {
           "Mares eat oats",
           "Does eat oats",
           "Little lambs eat ivy",
           "A kid will eat ivy too"
       };
       Random random = new Random();

       for (int i = 0;
            i < importantInfo.length;
            i++) {
           drop.put(importantInfo[i]);
           try {
               Thread.sleep(random.nextInt(5000));
           } catch (InterruptedException e) {}
       }
       drop.put("DONE");
   }
}
El hilo consumidor, definido en Consumer, simplemente recupera los mensajes y los imprime, hasta que recupera la cadena “DONE”. Este hilo también pausa por intervalos aleatorios.


import java.util.Random;

public class Consumer implements Runnable {
   private Drop drop;

   public Consumer(Drop drop) {
       this.drop = drop;
   }

   public void run() {
       Random random = new Random();
       for (String message = drop.take();
            ! message.equals("DONE");
            message = drop.take()) {
           System.out.format("MESSAGE RECEIVED: %s%n", message);
           try {
               Thread.sleep(random.nextInt(5000));
           } catch (InterruptedException e) {}
       }
   }
}
Finalmente, aquí está el hilo main, definido en ProducerConsumerExample, que lanza los hilos productor y consumidor.


public class ProducerConsumerExample {
   public static void main(String[] args) {
       Drop drop = new Drop();
       (new Thread(new Producer(drop))).start();
       (new Thread(new Consumer(drop))).start();
   }
}




Nota: la clase Drop fue escrita para demostrar bloqueos vigilados. Para evitar reinventar la rueda, examine estructuras de datos existentes en el Framework de Colecciones de java antes de intentar codificar sus propias estructuras para compartir objetos. Para más información, vaya a la sección de Preguntas y Ejercicios.




Popular Posts

Pedro Rozo. Powered by Blogger.