Empezando con concurrencia en Java

Vamos a explicar cómo implementa Java la concurrencia en sus programas en líneas muy básicas y generales. Son por todos conocidas las bondades de la concurrencia (y sus infinitas maldades ocultas) pero, de no ser éste tu caso, diremos que es la concurrencia a grandes rasgos en cuanto a nuestro campo se refiere.

Un programa simple, empieza su ejecución en el método main y consta de ese único hilo de ejecución. El método empieza, se ejecutan sus instrucciones secuencialmente y se finaliza su ejecución al llegar al final de main. Bien, lo que la concurrencia nos va a permitir es disponer de más de un hilo de ejecución dentro de nuestro propio programa. En nuestro programa haremos que el proceso padre main cree los hilos hijos que considere para realizar sus tareas. En contraste con los procesos concurrentes, que son independientes entre sí, los hilos de un mismo proceso comparten recursos como el espacio de direcciones virtuales y los recursos del sistema operativo. De esta forma, todos los hilos de un mismo proceso tienen acceso a los datos y procedimientos del proceso. Sin embargo, cada hilo posee su propio contador de programa y pila de llamadas a métodos.

En lenguaje Java disponemos de tres formas de implementar la concurrencia: mediante herencia de la clase Thread, implementando la interfaz Runnable ó utilizar objetos de la clase ThreadPoolExecutor. De ésta última no hablaremos en este post.

La diferencia entre las dos primeras posibilidades (sin entrar en profundidad) es básicamente que implementar la interfaz Runnable será útil a la hora de necesitar lanzar hilos sueltos ó bien si queremos dotar de concurrencia a una clase que ya es base de una jerarquía y que no podría heredar por tanto de Thread ya que en Java no existe la herencia múltiple (simulaciones aparte). En ambos casos es indispensable que nuestra clase implemente el método heredado public void run() donde definiremos el comportamiento de nuestros hilos ya que, por defecto, no hace nada. Aquí vemos dos de las formas de crear hilos en Java:

public class hiloRunnable implements Runnable{

	public static void main(String[] args){
		hiloRunnable run = new hiloRunnable();
		Thread hilo = new Thread(run);
		hilo.start();
	}
	@Override
	public void run(){
		System.out.println("Soy "+this.toString()+". Hola mundo!!!");
	}
}
public class hiloHerencia extends Thread{

	public static void main(String[] args){

		hiloHerencia hilo = new hiloHerencia();
		hilo.setName("un hilo que hereda de Thread. ");
		hilo.start();
	}

	public void run(){
		System.out.println("Soy "+getName()+"Hola mundo!!!");
	}
}

Aquí pondré un ejemplos para ilustrar el uso de concurrencia en programas Java además del uso de algunos de los métodos que se nos ofrecen. Decir antes que nada que haremos concurrencia ‘a pelo’. Pueden darse errores, si usáramos variables comunes, debidos a que no controlaremos el acceso a ellas. Si alguien está interesado puede indagar un poco empezando por los algoritmos de exclusión mutua como el algoritmo de Dekker ó el de la panadería de Lamport.

import java.util.Vector;

public class masHilos extends Thread{

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

		Vector hilos = new Vector();

		// Creamos 5 hilos.
		for(int i = 0; i < 5; i++){
			masHilos hilo = new masHilos();
			hilo.setName("Hilo numero "+(i+1));

			// Les asignamos distinta prioridad
			if(i%2 == 0) hilo.setPriority(MAX_PRIORITY);
			else hilo.setPriority(MIN_PRIORITY);

			hilos.add(hilo);
		}
		// Iniciamos los hilos
		for(masHilos h : hilos)
			h.start();

		// Hacemos que al terminar un hilo espere a los demas
		for(masHilos h : hilos)
			h.join();
	}

	public void run(){
		for(int i = 0; i < 10; i++){
			System.out.print(this.getName()+". Iteración: "+i);
			System.out.println(". Prioridad: "+this.getPriority());
		}
	}
}

En este código podemos ver el uso de los métodos start, join, setName, getName, setPriority y getPriority. En este ejemplo vemos dos cosas importantes. Si ejecutamos el programa varias veces podemos comprobar cómo los hilos se ejecutan sin un orden prefijado, casi aleatoriamente (una de las fuentes de problemas de la programación concurrente). A esto se le llama entrelazado de instrucciones. También podemos observar como las prioridades asignadas no nos servirían de mucho para resolver un orden entre los hilos ya que al parecer se saltan la restricción de prioridad cuando quieren. Esto es debido a que no podemos controlar el mapping de prioridades JVM-Sistema Operativo. El planificador del sistema operativo, que es el encargado de asignar el procesador a los hilos y procesos que lo soliciten, está fuera de nuestro control si usamos estas técnicas. En futuros posts profundizaremos un poco más en este tema.