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:
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