Java Series – Desmitificando Multithreading

Uuu ya con ese titulo ni ganas de leer este post verdad? xD  Pero tranquilidad que por la misma razón se llama “Desmitificando”, porque no es algo tan del otro mundo tener una aplicacion de esas caracteristicas en JAVA.
Hace algunos anhos cuando la tecnología de múltiples procesadores empezó a ubicarse en las maquinas de escritorio, no solo genero un impacto a nivel de hardware, sino también a nivel de software, ya que de alguna u otra manera nos dio la posibilidad de dar mas procesamiento a los equipos clientes, permitiéndonos mezclar el paradigma de Thin client, con un aprovechamiento real de todos los recursos de hardware sobre los que corren nuestras aplicaciones.
A estas alturas de la vida, el no utilizar al máximo los ciclos de CPU en nuestras aplicaciones no es solo un crimen tecnológico sino también un atentado al medio ambiente, y de esta premisa nacen conceptos como el Grid Computing, donde pones a disposición el tiempo ocioso de tu CPU para ser aprovechado por otros. En fin, el problema de trasfondo es que el hardware esta evolucionando mucho mas rápido que el software, y no todas las aplicaciones están preparadas para aprovechar la infraestructura sobre la que están siendo utilizadas, generando desuso de recursos.
En una experiencia previa, teníamos un cliente con un servidor de 8 procesadores y una cantidad considerable de RAM, y resulta que su personal de IT nos reporto como un Issue el hecho de que nuestra aplicación del lado servidor no estaba utilizando todos los recursos disponibles, es decir; le hicieron un monitoreo al uso de los procesadores cuando se ejecutaban procesos masivos y vieron que solo uno se utilizaba al máximo, el resto eran ciclos ociosos de cpu… Así que como buenos Ingenieros y Arquitectos de Software nos toco reestructurar la aplicación e identificar los procesos que se podían hacer de manera paralela para lanzar múltiples procesos y sacarle el jugo (como decimos por acá)  a los procesadores.
Si recordamos esas divertidas clases de Sistemas Operativos, se nos viene a la mente el gráfico del ciclo de un proceso donde la premisa era que un solo proceso corría a la vez, pero ahora eso varia dependiendo de la capacidad de los procesadores (Quad Core (4), I7(6), etc) y pues, como hacemos que nuestra aplicación utilice todos estos recursos??? A traves de Threads (Hilos).
(No podía omitir toda la historia anterior como introducción al tema, pero ya estamos en la parte interesante: el ejemplo! )
La mayoría de los lenguajes nos proveen las herramientas necesarias para cumplir con el objetivo de hacer aplicaciones multithreading (.Net, Scala, Google’s GO, etc, etc) y Java no es la excepción con sus clases para manejar threading.
El uso de threads implica básicamente identificar los procesos que se pueden ejecutar en paralelo, y una vez identificado no es complicado implementarlo; los dolores de cabeza se producen cuando tenemos que mezclar ese modelo con reacciones en UI (que levanten la mano los que alguna vez vimos una RED CROSS OF DEATH, o en .net el DataTable Index Corrupted) ya que los controles, y ciertas estructuras solo pueden ser manipuladas por un hilo a la vez.
El ejemplo de este post toma 2 procesos para realizar operaciones con threads:

  • Ejecutar asincronicamente una busqueda en iTunes de canciones por artista (Evita congelar el UI)
  • Simular un proceso de almacenamiento masivo de los resultados (Multithreading)

Es una molestia que se congele el UI mientras esperamos respuesta a la ejecución de un proceso, y un usuario es feliz si su aplicación le dice que esta haciendo, mientras que si se queda la pantalla congelada dirá “se colgó esta vaina” y seguramente le hara un kill como sea posible y llamara a service desk. Este caso esta representado en ejemplo de buscar a través del iTunes Store Search API, debido a que es una búsqueda en un servidor externo a través de la web, y dependemos básicamente de la latencia en la transmisión de datos para que la respuesta congele el UI hasta que se complete el request.
Hacemos la consulta esperando como resultado un texto en JSon y con la libreria Gson lo desarmamos en un abrir y cerrar de ojos, para luego mostrarlo en un JTable, así que fácilmente podemos identificar la parte del proceso que debe ser asincronica: La consulta al servidor web y el procesamiento de los resultados hasta que están en el formato esperado por el modelo del JTable.
Hasta ahí cumplimos con la primera parte de la premisa que era identificar la parte del proceso que podemos hacer asincronica, ahora la segunda parte es interactuar con el UI, para lo cual JAVA se ha encargado de abstraernos de este problema y desde su versión 6 incluye oficialmente al SwingWorker, el cual se encarga de ejecutarse en un contexto en el cual se ejecuta en background sin congelar el UI y permitiéndonos interactuar con los controles; la declaracion de la clase debe heredar de SwingWorker

public class iTunesSearchHandler extends SwingWorker<Object, Object>

Lo que queremos que se ejecute asincronicamente lo colocamos en el metodo doInBackground y para hacer alguna acción cuando termina el proceso utilizamos el metodo done

@Override
    protected Object doInBackground() throws Exception {
        label.setVisible(true);
        URL url = new URL(searchUrl);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("GET");
        connection.setReadTimeout(15 * 1000);
        connection.connect();
        BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
        StringBuilder stringBuilder = new StringBuilder();
        String line = null;
        while ((line = reader.readLine()) != null) {
            stringBuilder.append(line + "\n");
        }
        parseJsonToList(stringBuilder.toString());

        return "";
    }

@Override
    protected void done() {
        super.done();
        label.setVisible(false);
    }

Las lineas donde cambia la visibilidad de un label es para mostrar el tipico “loading” en la pantalla mientras se ejecuta el request y así sabemos que esta haciendo la aplicación.
Como indique anteriormente la otra parte del ejemplo es simular un procesamiento masivo de la información que tenemos en memoria y típicamente es almacenarla en algún lugar; es importante identificar estas operaciones de base de datos como las típicas que podemos realizar paralelamente, (si son datos dependientes o referenciados entre si es vital manejar sincronizacion de procesos) ya que de nuestro DBMS esperamos un modelo ACID que nos garantice la integridad de la información. Por lo cual de manera descabellada vamos a lanzar un thread para grabar, el cual lanzara un nuevo thread por cada registro a guardar! 🙂

@Override
    protected Object doInBackground() throws Exception {
        label.setVisible(true);
        for(iTunesSearchResult item : data)
        {
            item.setRowIndex(data.indexOf(item));
            new iTunesSaveHandler(item).execute();
        }
        return "";
    }

Que conseguimos con eso? Pues que como ningún registro es dependiente del otro cada uno se ejecuta como una transacción independiente y ponemos a trabajar los procesadores al tener que ejecutar muchos procesos simultáneos 😛

Para mostrar el resultado del proceso, podemos ver con el jConsole los hilos que se ejecutan y ademas en el output vemos los mensajes donde el orden de almacenamiento no es necesariamente el orden en el que se fueron ejecutando los hilos:

Pantalla de aplicacion demo


Monitoreo antes de proceso masivo


Monitoreo despues de proceso masivo


Log de transacciones

Como conclusiones de los prints anteriores, podemos ver que en el monitoreo después de ejecutar la simulación de almacenamiento masivo se incrementan la cantidad de threads en la aplicación y el uso de cpu; mientras que en el log de transacciones podemos ver que los registros se van procesando no necesariamente en el orden que fueron lanzados, ya que cada transacción es independiente y finalmente depende de la prioridad que le asigne el cpu al thread para su ejecución.

Saludos,
gish@c

Descargar Proyecto Netbeans 6.8