Ir al contenido principal

Introducción a Hilos con Java

El presente articulo, espera ser una pequeña introducción al manejo de hilos en Java, donde al final el lector puede disparar y manipular al menos de forma básica programas multitarea. Como pequeña introducción, podemos decir que los computadores de escritorio actuales, así como los servidores, poseen gran capacidad para la ejecución concurrente/multitarea, es decir, podemos aprovechar el procesador para ejecutar mas de una tarea al mismo tiempo e inclusive en "background", utilizando demonios.

Java, como un lenguaje de programación moderno, nos ofrece en la versión 6, un manejo muy amplio de hilos, proporcionando no solo la posibilidad de disparar tareas (hilos) o demonios, también podemos crear, bloqueos, semáforos, monitores, sincronización, etc.

Así pues, iniciamos este articulo, presentando la creación de un simple hilo:

La forma mas simple, pero no mas correcta de hacer un hilo, es simplemente extender de la clase: Thread; veamos un ejemplo:




//---------------------------------------
package cr.ticoblogger.jsanca.thread;


public class CurrentThread extends Thread {


}
//---------------------------------------


Esta es la definición básica de un hilo, este puede ser disparado de una forma muy simple de la siguiente manera:


//---------------------------------------
CurrentThread currentThread = new CurrentThread();
currentThread.start();
//---------------------------------------


Esto da como resultado, la ejecución de un hilo, aunque algo tonto, pues solo nace y muere seguidamente. Ahora bien si deseamos agregar lógica al hilo, debemos sobre escribir, el método: run, veamos:






//---------------------------------------

package cr.ticoblogger.jsanca.thread;

public class CurrentThread extends Thread {

@Override
public void run() {

System.out.println("Ejecutando un hilo nuevo");
}

public static void main(String[] args) {

CurrentThread currentThread = new CurrentThread();
currentThread.start();
}
}

//---------------------------------------





Con este simple código, ya estamos ejecutando alguna tarea en nuestro hilo. En nuestro simple caso, simplemente imprimimos un mensaje en la consola, pero también podemos ejecutar código mas complejo, como checkar una base de datos, monitorear otros hilos, abrir archivo paralelamente, etc. Una acotación muy importante, en el método run, ejecutamos nuestra lógica para el hilo, pero es con start que le indicamos al sistema operativo que dispare nuestro hilos. Esto es muy importante, pues si llamamos directamente a run, simplemente ejecutaremos la lógica, pero de forma tradicional, como si llamáramos a cualquier otra clase, es decir secuencial en lugar de paralelo. Ahora, agreguemos algo mas de código a nuestro Thread, para abordar otros métodos interesantes:




//---------------------------------------
package cr.ticoblogger.jsanca.thread;

import java.util.Date;

public class CurrentThread extends Thread {

public void hacerAlgoDormir() {

Date date = new Date();
System.out.println("Iniciando el hilo");

try {

Thread.sleep(2000, 10);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("Duracion total: " + (new Date().getTime() - date.getTime())
/ 1000 + " s");
}

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

CurrentThread currentThread = new CurrentThread();
currentThread.start();

System.out.println("Termino el main");
}

@Override
public void run() {

this.hacerAlgoDormir();
}

}
//---------------------------------------



Analicemos este ejemplo paso a paso.

Primero declaramos nuestra clase thread, seguidamente lo disparamos como un hilo y a continuación imprimimos un mensaje, indicando que el hilo actual (Main), ha terminado.
Asi pues, este nuevo hilo disparado, inicia ejecutando run, que a su vez invoca a hacerAlgoDormir;

Primero declaramos un objeto Date, que tendrá la fecha y el instante actual e imprimimos iniciando el hilo.

Segundo, encontramos una sentencia try/catch, con la invocación al método, sleep:
El método sleep, básicamente duerme por una cantidad de tiempo determinada nuestro hilo, el tiempo es dado en milisegundo y nano segundo, en nuestro caso son 2000 milisegundos y
10 nanosegundos, o lo que es lo mismo, 2 segundo y 10 nano segundos. Como dije, esto detiene (pausa), el hilo en un intervalo determinado de tiempo y después continua su ejecución.
Por ultimo, observar que estamos capturando la excepción InterruptedException, esto se debe a que puede que nuestro hilo sera interrumpido por otro hilo o también que nuestro sistema
operativo, detenga o interrumpa nuestro hilo. Dependiendo del sistema operativo, a veces debemos detener la ejecución del hilo por un momento para dar paso a otro hilo, de lo contrario, el pipeline de hilos, nos interrumpirá, pues nos identifica con un hilo que esta consumiendo demasiados recursos, pues ello, es bueno controlar esta excepción.

En versiones anteriores, este método es accedido invocando a Thread.currentThread().sleep().

Por ultimo, se realiza un calculo de la duración del hilo, a continuación el resultado:

Termino el main
Iniciando el hilo
Duración total: 2 s


Notara que nuestro hilo main termina antes que el hilo hijo su ejecución (aunque este permanece vivo esperando que el hilo termine), pero el código es ejecutado paralelamente.

Que pasaría si tenemos 10 hilos o tareas y no queremos seguir el flujo, hasta que todos estos hilos terminen su ejecución paralela, bueno para ello podemos utilizar el método: join.

Este método, permite que el hilo padre, que dispara los hilos, detenga su ejecución, hasta que el hilo hijo termine la suya, veamos el ejemplo modificado:




//---------------------------------------
package cr.ticoblogger.jsanca.thread;

import java.util.Date;

public class CurrentThread extends Thread {

public void hacerAlgoDormir() {

Date date = new Date();
System.out.println("Iniciando el hilo");

try {

Thread.sleep(2000, 10);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("Duracion total: " + (new Date().getTime() - date.getTime())
/ 1000 + " s");
}

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

CurrentThread currentThread = new CurrentThread();
currentThread.start();
currentThread.join();
System.out.println("Termino el main");
}

@Override
public void run() {

this.hacerAlgoDormir();
}

}

//---------------------------------------




Si vemos la salida, notaremos que la ejecución se torna algo mas secuencial, pero tome en cuenta que el hilo current thread es ejecutado en paralelo, es lo mismo que el caso anterior,
sencillamente, el hilo principal, esta esperando a que este termine para seguir ejecutandose, mediante el uso del método join.

Un ejemplo, mas complejo:

Como ejercicio final, vamos a ejecutar un ejemplo mas complejo, el siguiente código básicamente define un wrapper para un FIleWriter y un hilo que puede bajar una pagina web valiéndose
de la librería java.net.

Veamos el ejemplo y posteriormente su explicación:




//---------------------------------------
package cr.ticoblogger.jsanca.thread;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

public class FileResultWriter {

private FileWriter fileWriter = null;

public FileResultWriter(File file) throws IOException {

fileWriter = new FileWriter (file);
}

public void writeOut (String string) {

try {
fileWriter.write(string);
} catch (IOException e) {
e.printStackTrace();
}
}

public void close () {

try {
this.fileWriter.flush();
this.fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}

}

}
//---------------------------------------

//---------------------------------------
package cr.ticoblogger.jsanca.thread;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Arrays;

public class RunnableThread implements Runnable {

FileResultWriter fileResultWriter = null;

public RunnableThread(String urlToDownload,
FileResultWriter fileResultWriter) {
super();
this.urlToDownload = urlToDownload;
this.fileResultWriter = fileResultWriter;
}



private String urlToDownload = null;

public void run() {

URL url = null;
URLConnection connection = null;
InputStream inputStream = null;
byte[] buffer = new byte[126];
int byteDownload = 0;
StringBuilder builder = new StringBuilder();
java.util.Date initialDate = new java.util.Date();
int bytes = 0;


try {

url = new URL(this.urlToDownload);
connection = url.openConnection();

connection.connect();
inputStream = connection.getInputStream();
while ((bytes = inputStream.read(buffer)) > 0) {

byteDownload += bytes;
builder.append(new String(buffer));
Arrays.fill(buffer, (byte) 0);
}

} catch (MalformedURLException e) {

e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {

if (null != inputStream) {

this.fileResultWriter
.writeOut("\n"
+ "--------------------------------------------------------------"
+ "\n");
this.fileResultWriter.writeOut(Thread.currentThread().getName()
+ "\n");
this.fileResultWriter.writeOut(url + " = " + "\n");
this.fileResultWriter.writeOut(builder + "\n");
System.out.println("Termino: " + this.urlToDownload);
System.out.println("Bytes download: " + (byteDownload)
+ " bytes");
System.out.println("En: "
+ (new java.util.Date().getTime() - initialDate
.getTime()) / 1000 + " s");

try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

} // run.

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

Thread thread = new Thread(new Runnable() {

public void run() {

System.out.println("Ejecutando un hilo dummy para indicar que el sistema inicia");
}

});

thread.start();

FileResultWriter fileResultWriter = new FileResultWriter(new File(
"./result.txt"));

Thread[] threads = new Thread[] {
new Thread(new RunnableThread("http://www.google.com",
fileResultWriter), "google"),
new Thread(new RunnableThread("http://www.google.com",
fileResultWriter), "google"),

new Thread(new RunnableThread(
"file:/Users/jonathansanchez/test.txt",
fileResultWriter), "test"),
new Thread(new RunnableThread("http://www.yahoo.com",
fileResultWriter), "yahoo"),
new Thread(new RunnableThread("http://www.hotmail.com",
fileResultWriter), "hotmail"),
new Thread(new RunnableThread("http://www.crjug.com",
fileResultWriter), "crjug"),
new Thread(new RunnableThread("http://jsanca.ticoblogger.com",
fileResultWriter), "google"),
new Thread(new RunnableThread("http://www.javahispano.org",
fileResultWriter), "javahispano"), };

for (Thread thread2 : threads) {

thread2.start();
}

System.out.println("bajando los urls");
for (Thread thread3 : threads) {

thread3.join();
}

fileResultWriter.close();
System.out.println("Todo termino.......");
}
}

//---------------------------------------





Como se indico anteriormente, la primera clase es un simple wrapper de un FileWriter, no lo abordaremos aquí.

Ahora vemos la clase: RunnableThread

Lo primero que debe notar, es que esta clase no extiende de Thread, si no que en su lugar, implementa Runnable; este enfoque en mi humilde criterio es el mas correcto, por
varios motivos, el primero de ellos, es que, al ser Java un lenguaje en el cual no existe la herencia multiple, heredar directamente de Thread limita nuestro diseño, el segundo problema
es que puede que la cohesión de la clase no sea la mejor, pues puede que un DAO por ejemplo, sea un Thread, lo cual puede que a nivel lógico no tenga mucho sentido, pero si a nivel técnico, en contraste, si extendemos de Runnable, ya no somos una clase thread, pero mediante la programación por contrato, simplemente manejamos la ejecución paralela mediante un aspecto mas de la clase. Así pues, implementamos el método run, para correr nuestro código concurrent; al final el hilo se reduce a pasar el objeto Runnable como constructor al Thread, lo que se le llama un target thread.

Dijo esto analicemos brevemente el ejemplo:
Lo primero que vemos, es un simple hilo anónimo, que indica que vamos a iniciar la ejecución del sistema, lo iniciamos y proseguimos.

Después, declaramos un objeto que nos permitirá almacenar los resultados y le pasamos un nombre de archivo.

Después declaramos un vector de hilos (una variante seria declarar un ThreadGroup, se los dejo de tarea :)

Como vemos a nuestros hilos, les podemos pasar el objeto runnable, al que se le pasa vía constructor el link que deseamos bajar, la clase para escribir y un nombre para el hilo.
Seguidamente, implementamos un foreach, y lanzamos uno a uno los hilos, los cuales inician su ejecución paralelamente.

Hacemos el mismo foreach, pero esperando que uno a uno de los hilos, terminen la ejecución.
Cerramos el archivo y imprimimos que termino la ejecución del sistema.

Vamos al hilo;

Inicialmente declaramos e inicializamos todas las variables que vamos a utilizar para bajar la pagina.

Seguidamente creamos un objeto URL y abrimos la conexion al server, nos conectamos y obtenemos el flujo de entrada (input).

Por ultimo, vamos leyendo mediante la técnica de bufereado el contenido de la pagina, en segmentos de 126 bytes.

Nótese, el uso de esta linea:

Arrays.fill(buffer, (byte) 0);

Esta, se utiliza para limpiar el buffer, pues puede que en la ultima leída, nos consuma todo el buffer y quede basura, otra técnica es tomar la cantidad de bytes
copiados e indicarle al StringBuilder que solo tome cierta cantidad de bytes del vector.

Por ultimo, se imprimen algunas métricas de la ejecución del hilo, si ejecutas este programas debes, crear un archivo llamado:

file:/Users/jonathansanchez/test.txt

O bien cambiar esa linea para leer un archivo de forma local, en tu computadora.

Por ultimo el resultado quedara en la carpeta raíz del programa, llamado result.txt

Un saludo,
J

Comentarios

Carlos P. dijo…
Este blog ha sido eliminado por un administrador de blog.
Marco dijo…
Este blog ha sido eliminado por un administrador de blog.
Gabriel Solano dijo…
Sin duda los hilos son una excelente manera de aprovechar al máximo los recursos. Si ga con la serie pero insisto, agregale un resaltador de sintáxis que asi se hace menos pesada la lectura del código.

Entradas más populares de este blog

Impensando acerca de las referencias en Java

Fue hace ya algún tiempo que pase un rato discutiendo con algunos compañeros acerca de si existe o no el paso por referencia; el discurso fue mucho hacia que en Java el comportamiento, en el supuestamente pasamos por referencia un objeto y por valor los objetos primitivos creo mucha polémica. Para ubicarnos en contexto veamos el siguiente ejemplo. public static void main(String[] args) { int value = 10; changeValue(value); System.out.println("value = " + value); User user = new User(); Name name = new Name(); user.setName(name); name.setName("jsanca"); name.setLastName("XXX"); user.setPassword("123queso"); System.out.println("user: " + user.getName().getName() + ", " + user.getName().getLastName() + ", " + user.getPassword()); changeValue1(user); System.out.println("user: " + user.getName().getName() + ", " + user.getName().getLastName() + ", " + user.ge...

Al fin MTV reconoce el potencial de la Internet

Después de mucha lucha y denuncias públicas y demás pleitos para llamar la atención, la cadena mundial de música por televisión mas grande el mundo MTV , abren toda su biblioteca (bueno casi toda, el material en ingles por el momento), al publico. Aseguran tener conciertos, espectáculos acústicos , más todos sus vídeos y a diferencia de YouTube (que es mantenida por una comunidad en buena parte), esta es soportada por MTV , lo que le proporciona idéntica calidad a cada uno de sus temas ( vídeos ), el sitio en donde han publicado el contenido le llaman MTV Music , y yo debo reconocer que estoy como chiquito con juguete nuevo, jejeje . El sitio ofrece una gran cantidad de música , como señale anteriormente y además realiza sugerencias acerca de música o vídeos relacionados, en las búsquedas , te de la oportunidad de ir a un perfil del artista o directamente a los vídeos . Buena noticia y supongo que a MTV le paso como dicta el viejo adajio , " si no puedes vencerlos, unet...

Ideas para un eco-hogar

Un Eco Hogar, Ultimamente he estado pensando al respecto (en la implementación de una casa ecológica), leyendo un poco me entero que existen diferentes alternativas para el ahorro de consumo electrico del hogar; paneles solares, mini hidro turbinas, energía eólica, etc. Algunas alternativas interesantes representan los termos calentados por paneles solares, para no gastar energía en la ducha caliente, etc. Todas estas alternativas están muy bien, aunque la inversión por el momento es algo grande para un hogar promedio, con el consumo masivo, podría convertirse en una opción de facto. Estas opciones representa un ahorro en el consumo eléctrico, pero que hay con el consumo del H2O; sin necesidad de ser muy observador, nos damos cuenta que uno de los mayores puntos donde se desperdicia agua son: el baño y la ducha. En cuanto a la ducha no se me ocurre mas que algunos habitos en vez de soluciones tecnicas, como mojarse, cerrar el tuvo, enjabonarse, etc. Cerrar el tuvo cuando no lo estamos ...