5 cosas que usted no sabía acerca de... java.util.concurrent, Parte 1

Programación multihilos con Colecciones concurrentes

La escritura de código multihilado que tenga buen desempeño y que proteja a las aplicaciones contra corrupciones es simplemente difícil — y es por lo cual tenemos java.util.concurrent. Ted Neward le muestra cómo las clases de Colecciones concurrentes como CopyOnWriteArrayList, BlockingQueue y ConcurrentMap , clases estándar de Colecciones modernizadas para sus necesidades de programación de concurrencia.

Ted Neward, Director, Neward & Associates

Ted NewardTed Neward es el director de Neward & Associates, donde hace consultoría, es mentor, enseña y hace presentaciones sobre Java, .NET, XML Services y otras plataformas. Vive en Seattle, Washington.



08-10-2012

Acerca de esta serie

¿Así que usted considera que sabe acerca de programación Java? El hecho es que la mayoría de los desarrolladores rasguñan la superficie de la plataforma Java, aprendiendo apenas lo necesario para realizar su trabajo. En estaserie, Ted Neward profundiza hacia el núcleo de la funcionalidad de la plataforma Java para descubrir datos poco conocidos que pueden ayudarle a resolver incluso los desafíos de programación más complicados.

Las Colecciones Concurrentes fueron una enorme adición a Java™ 5, pero muchos desarrolladores Java les perdieron de vista con toda la algarabía acerca de anotaciones y genéricos. Adicionalmente (y tal vez con mayor sinceridad), muchos desarrolladores evitan este paquete porque asumen que, tal como los problemas que pretende resolver, debe ser complicado.

De hecho, java.util.concurrent contiene muchas clases que resuelven efectivamente muchos problemas comunes de concurrencia, sin requerir que usted siquiera sude una gota. Continúe leyendo para conocer cómo las clases java.util.concurrent como CopyOnWriteArrayList y BlockingQueue le ayudan a resolver los desafíos perniciosos de la programación multihilos.

1. TimeUnit

Desarrolle habilidades de este tema

Este contenido es parte de knowledge paths progresivo para avanzar en sus habilidades. Vea:

Aunque no es una clase Collections per se, la enumeraciónjava.util.concurrent.TimeUnit hace que el código sea mucho más fácil de leer. UtilizarTimeUnit libera a los desarrolladores que usan su método o API de la tiranía del milisegundo.

TimeUnit incorpora todas las unidades de tiempo, variando desde MILLISECONDS y MICROSECONDS hasta DAYS y HOURS, lo cual significa que maneja casi todos rangos de tiempo que un desarrollador puede necesitar. Y, gracias a los métodos de conversión declarados en la enumeración, incluso es trivial convertir HOURS enMILLISECONDS cuando el tiempo se acelera.


2. CopyOnWriteArrayList

Crear una copia fresca de un array es una operación demasiado costosa, en términos tanto de tiempo como de sobrecosto de memoria, como para considerarla para uso ordinario; en su lugar, los desarrolladores a menudo usan como recurso ArrayList . Sin embargo, esa también es una opción costosa, porque cada vez que usted itera en el contenido de la colección, debe sincronizar todas las operaciones, incluyendo lectura y escritura, para garantizar la consistencia.

Esto representa un retroceso en la estructura de costos para escenarios donde numerosos lectores están leyendo la ArrayList pero cuando pocos la están modificando.

CopyOnWriteArrayList es la pequeña joya asombrosa que resuelve este problema. Su Javadoc define a CopyOnWriteArrayList como una "variante de hebra segura de ArrayList en la cual todas las operaciones (añadir, configurar, etc.) son implementadas mediante la creación de una copia fresca del array".

La colección copia internamente su contenido sobre un nuevo array tras cualquier modificación, de manera que los lectores que acceden al contenido del array no incurren en costos de sincronización (pues nunca están operando sobre datos mutables).

Esencialmente, CopyOnWriteArrayList es ideal para el escenario exacto donde ArrayList nos falla: colecciones de lectura-frecuente y escritura-ocasional como las Listener para un evento JavaBean.


3. BlockingQueue

La interfaz BlockingQueue establece que es una Queue, Lo cual significa que sus elementos se almacenan en un pedido tipo primero en entrar, primero en salir (FIFO). Los elementos insertados en un pedido en particular son recuperados en ese mismo pedido — pero con la garantía adicional de que cualquier intento de recuperar un elemento de una cola vacía bloqueará la hebra de llamada hasta que el elemento esté listo para ser recuperado. De igual forma, cualquier intento de insertar un elemento dentro de una cola que esté llena bloqueará la hebra de llamada hasta que haya espacio disponible en el almacenamiento de la cola.

BlockingQueue resuelve pulcramente el problema de cómo "entregar" elementos reunidos por una hebra a otra hebra para su procesamiento, sin una preocupación explícita por problemas de sincronización. La pista Guarded Blocks del Tutorial Java es un buen ejemplo. Esta construye un almacenamiento intermedio vinculado de ranura individual usando sincronización manual y wait()/notifyAll() para señalar entre hebras cuando haya un nuevo elemento disponible para consumo, y cuando la ranura está lista para ser llenada con un nuevo elemento. (Vea la sección implementación de Guarded Blocks para más detalles).

A pesar del hecho de que el código del tutorial de Guarded Blocks funciona, este es extenso, desordenado y no es del todo intuitivo. Regresando a los primeros días de la plataforma Java, sí, los desarrolladores Java tenían que enredarse con dicho código, pero estamos en el 2010 — ¿seguramente las cosas han mejorado?

El Listado 1 muestra una versión reescrita de código Guarded Blocks donde he empleado ArrayBlockingQueue en lugar del manualmente escrito Drop.

Listado 1. BlockingQueue
import java.util.*;
import java.util.concurrent.*;

class Producer
    implements Runnable
{
    private BlockingQueue<String> drop;
    List<String> messages = Arrays.asList(
        "Mares eat oats",
        "Does eat oats",
        "Little lambs eat ivy",
        "Wouldn't you eat ivy too?");
        
    public Producer(BlockingQueue<String> d) { this.drop = d; }
    
    public void run()
    {
        try
        {
            for (String s : messages)
                drop.put(s);
            drop.put("DONE");
        }
        catch (InterruptedException intEx)
        {
            System.out.println("Interrupted! " + 
                "Last one out, turn out the lights!");
        }
    }    
}

class Consumer
    implements Runnable
{
    private BlockingQueue<String> drop;
    public Consumer(BlockingQueue<String> d) { this.drop = d; }
    
    public void run()
    {
        try
        {
            String msg = null;
            while (!((msg = drop.take()).equals("DONE")))
                System.out.println(msg);
        }
        catch (InterruptedException intEx)
        {
            System.out.println("Interrupted! " + 
                "Last one out, turn out the lights!");
        }
    }
}

public class ABQApp
{
    public static void main(String[] args)
    {
        BlockingQueue<String> drop = new ArrayBlockingQueue(1, true);
        (new Thread(new Producer(drop))).start();
        (new Thread(new Consumer(drop))).start();
    }
}

El ArrayBlockingQueue también honra lo "justo" — queriendo decir que puede proporcionar acceso de hebras de lector y grabador del tipo primera en entrar, primera en salir. La alternativa sería una política más eficiente que corra el riesgo de privar de recursos a algunas hebras. (Esto es, sería más eficiente permitir a los lectores ejecutarse mientras otros lectores mantienen el bloqueo, pero usted se arriesga a un flujo constante de hebras de lectura evitando que el grabador realice su trabajo).

¡Cuidado con los errores!

Por cierto, usted está en lo cierto si notó que Guarded Blocks contiene un error enorme — ¿qué pasaría si un desarrollador sincroniza en la instancia Drop dentro de main()?

BlockingQueue también soporta métodos que toman un parámetro de tiempo que indica cuánto tiempo debe bloquearse la hebra antes de retornar a la falla de señal para insertar o recuperar el elemento en cuestión. Hacer esto evita una espera desvinculada, la cual podría ser la muerte de un sistema de producción, pues una espera desvinculada puede convertirse muy fácilmente en un sistema colgado haciendo necesario un reinicio.


4. ConcurrentMap

Map aloja un error sutil de concurrencia que ha llevado a muchos desarrolladores Java incautos a perderse. ConcurrentMap es la solución fácil.

Cuando se accede a un Map desde múltiples hebras, es común usar containsKey() o get() para encontrar si una clave dada está presente antes de almacenar el par de clave/valor. Pero incluso con un Map sincronizado, alguna hebra podría escabullirse durante este proceso y tomar el control del Map. El problema es que el bloqueo es adquirido al inicio delget() y luego liberado antes de que el bloqueo se pueda adquirir de nuevo, en la llamada a put(). El resultado es una condición de actualización: es una competencia entre las dos hebras y el resultado será diferente dependiendo de cuál llegue primero.

Si dos hebras llaman un método exactamente al mismo tiempo, cada una realizará prueba y cada una realizará put, perdiéndose en el proceso el valor de la primera hebra. Afortunadamente, la interfaz ConcurrentMap soporta cierto número de métodos adicionales que están diseñados para hacer dos cosas bajo un bloqueo individual: putIfAbsent(), por ejemplo, realiza la primera prueba y luego realiza un put solo si la clave no está almacenada en el Map.


5. SynchronousQueues

SynchronousQueue es una criatura interesante, según el Javadoc:

Es una cola de bloqueo en la cual cada operación de inserción debe esperar por una operación de eliminación correspondiente de otra hebra y viceversa. Una cola sincronizada no tiene ninguna capacidad interna, ni siquiera la capacidad de una.

Esencialmente, SynchronousQueue es otra implementación de la anteriormente mencionada BlockingQueue. Esta nos proporciona una forma extremadamente ligera de intercambiar elementos de una hebra a otra usando la semántica de bloqueo usada porArrayBlockingQueue. En el Listado 2, reescribí el código del Listado 1 usando SynchronousQueue en lugar de ArrayBlockingQueue:

Listado 2. SynchronousQueue
import java.util.*;
import java.util.concurrent.*;

class Producer
    implements Runnable
{
    private BlockingQueue<String> drop;
    List<String> messages = Arrays.asList(
        "Mares eat oats",
        "Does eat oats",
        "Little lambs eat ivy",
        "Wouldn't you eat ivy too?");
        
    public Producer(BlockingQueue<String> d) { this.drop = d; }
    
    public void run()
    {
        try
        {
            for (String s : messages)
                drop.put(s);
            drop.put("DONE");
        }
        catch (InterruptedException intEx)
        {
            System.out.println("Interrupted! " + 
                "Last one out, turn out the lights!");
        }
    }    
}

class Consumer
    implements Runnable
{
    private BlockingQueue<String> drop;
    public Consumer(BlockingQueue<String> d) { this.drop = d; }
    
    public void run()
    {
        try
        {
            String msg = null;
            while (!((msg = drop.take()).equals("DONE")))
                System.out.println(msg);
        }
        catch (InterruptedException intEx)
        {
            System.out.println("Interrupted! " + 
                "Last one out, turn out the lights!");
        }
    }
}

public class SynQApp
{
    public static void main(String[] args)
    {
        BlockingQueue<String> drop = new SynchronousQueue<String>();
        (new Thread(new Producer(drop))).start();
        (new Thread(new Consumer(drop))).start();
    }
}

El código de implementación se ve casi idéntico, pero la aplicación tiene un beneficio agregado en cuanto a que SynchronousQueue permitirá una inserción en la cola solo si hay una hebra esperando a consumirla.

En la práctica, SynchronousQueue es similar a los "canales rendezvous" disponibles en lenguajes como Ada o CSP. Estos también se conocen algunas veces como "uniones" en otros entornos, incluyendo .NET (vea Recursos).


En conclusión

¿Por qué luchar introduciendo concurrencia en sus clases Collections cuando la biblioteca de tiempo de ejecución Java ofrece equivalentes útiles y pre elaborados? El siguiente artículo de esta serie explora aún más el java.util.concurrent namespace.


Descargar

DescripciónNombretamaño
Sample code for this articlej-5things4-src.zip23KB

Recursos

Aprender

Comentar

Comentarios

developerWorks: Ingrese

Los campos obligatorios están marcados con un asterisco (*).


¿Necesita un IBM ID?
¿Olvidó su IBM ID?


¿Olvidó su Password?
Cambie su Password

Al hacer clic en Enviar, usted está de acuerdo con los términos y condiciones de developerWorks.

 


La primera vez que inicie sesión en developerWorks, se creará un perfil para usted. La información en su propio perfil (nombre, país/región y nombre de la empresa) se muestra al público y acompañará a cualquier contenido que publique, a menos que opte por la opción de ocultar el nombre de su empresa. Puede actualizar su cuenta de IBM en cualquier momento.

Toda la información enviada es segura.

Elija su nombre para mostrar



La primera vez que inicia sesión en developerWorks se crea un perfil para usted, teniendo que elegir un nombre para mostrar en el mismo. Este nombre acompañará el contenido que usted publique en developerWorks.

Por favor elija un nombre de 3 - 31 caracteres. Su nombre de usuario debe ser único en la comunidad developerWorks y debe ser distinto a su dirección de email por motivos de privacidad.

Los campos obligatorios están marcados con un asterisco (*).

(Por favor elija un nombre de 3 - 31 caracteres.)

Al hacer clic en Enviar, usted está de acuerdo con los términos y condiciones de developerWorks.

 


Toda la información enviada es segura.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=90
Zone=tecnologia Java
ArticleID=839599
ArticleTitle=5 cosas que usted no sabía acerca de... java.util.concurrent, Parte 1
publish-date=10082012