Contenido


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

Programación multihilos con Colecciones concurrentes

Comments

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).

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.


Recursos para Descargar


Temas relacionados


Comentarios

Inicie Sesión o Regístrese para agregar comentarios.

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