Me ha parecido importante empezar a checar como sería hacer algunas cosas que puedo hacer en Python, en Java, este post lo dedico a explicar como se crean y funcionan los hilos en Java, así como sincronizarlos, etcétera.
Los programas multihilo tienen dos o más partes que pueden ejecutarse de manera simultanea o concurrente. Cada parte del programa se llama hilo y cada hilo sigue un camino diferente. La programación multihilo es una manera especializada de multitarea.
Hay dos tipos de multitarea, a travez de procesos y atravez de hilos. Ambas son diferentes un proceso es un programa que se esta ejecutando, entonces la multitarea que se basa en procesos es la capacidad de las computadoras de ejecutar varios programas a la vez, el programa es la unidad más pequeña que el sistema de computo puede manejar.
La multitarea que se basa en hilos, el hilo es la unidad de código mas pequeño que se puede gestionar. Es decir un programa es capaz de ejecutar dos o más tareas en simultáneo.
La multitarea que se basa en hilos tiene un menor costo de operación que las basadas en procesos. Los procesos requieren de más recursos y necesitan espacio de direccionamieto propio. La comunicacion con procesos es costosa y limitada. Intercambiar contextos de procesos tambien es costoso. Mientras que los hilos son ligeros, comparten el mismo espacio de direccionamiento y el mismo proceso.
Los programas de Java usan entornos multitarea basados en procesos, pero esto no esta bajo control de Java. La multitarea de hilos si esta al control de Java. La multitarea que se basa en hilos permiten escribir programas eficientes que usan optimamente el CPU.
Clase Thread y la interfaz Runnable
El sistema multihilo de Java se crean en torno a la clase Thread, sus métodos y su interfaz Runnable. La clase Thread encapsula a un hilo de ejecución. Como no se puede hacer referencia directamente al estado de un hilo que se esta ejecutando es necesario usar la instancia a la clase Thread que representa a dicho hilo. Para hacer un hilo nuevo el programa debe extender a la clase Thread o implementar la interfaz Runnable.
La clase Thread define métodos que ayudan a manejar los hilos. Estos son algunos:
- getName: Obtiene el nombre del hilo.
- getPriority: Obtiene la prioridad del hilo.
- isAlive: Determina si un hilo aún se esta ejecutando.
- join: Espera a que termine un hilo.
- run: Punto de entrada de un hilo.
- sleep: Suspende a un hilo durante cierto tiempo.
- start: Empieza al hilo llamando a run.
Hilo principal
Cuando los programas comienzan su ejecución, hay un hilo que se ejecuta automáticamenre, este es el hilo principal ya que es el único que se ejecuta al iniciar el programa. Este hilo es importante por dos razones:
- A partir de este hilo será posible crear el resto de los hilos del programa.
- Este es el último que finaliza su ejecución ya que debe de realizar ciertas acciones para terminar correctamente el programa.
Para controlar este hilo, hay que crear un objeto Thread, entonces se crea una referencia al mismo con el método
currentThread(), que es un miembro public static de la clase Thread, su forma general es la siguiente:
static Thread currentThread()
Ese método devuelve una referencia al mismo hilo del que fué llamado. Una vez que se obtuvo la referencia al hilo principal es posible controlarlo como a cualquier otro hilo.
Ejemplo de Control de hilo principal
//controlamos el hilo principal
class hiloPrincipal{
public static void main(String args[]){
Thread t = Thread.currentThread();
System.out.println("Hilo actual:" + t);
//cambio del nombre del hilo
t.setName("Hilo principal");
System.out.println("Ahora se llama: " + t);
try{
for(int n = 5; n > 0; n--){
System.out.println(n);
Thread.sleep(1000);
}
}
catch (InterruptedException e){
System.out.println("Se interrumpio al hilo principal");
}
}
}
Con este programa obtenemos una referencia al hilo actual, llamando al método
currentThread(), esta referencia la almacené en la variable local t. Llamamos al método
setName() para poder cambiar el nombre interno del hilo.
El ciclo cuenta del 5 al 1 pausandose entre cada número 1 segundo, con el método sleep.
Creación de un hilo
Los hilos en Java se crean con el objeto del tipo Thread.java y hay dos maneras de hacer esto:
- Implementando la interfaz Runnable.
- Extendiendo la clase thread.
Interfaz Runnable
La manera más facil de crear un hilo es creando una clase que implemente la interfaz Runnable, se puede construir un hilo desde cualquier objeto que implementa esta interfaz. La clase necesita implementar el método run(), que se declara asi:
public void run()
Dentro del método run(), se escribe el código que definirá un nuevo hilo.
Después de crear la clase, se crea el objeto de tipo Thread:
Thread(Runnable objeto Hilo, String nombreHilo)
El nuevo hilo que se creo no empezará su interfaz hasta que se llame al metodo start(), start() ejecuta una llamada al método run():
void start()
Ejemplo de creación de un hilo con Runnable:
//creacion de un segundo hilo
class HiloNuevo implements Runnable{
Thread t;
HiloNuevo(){
t = new Thread(this, "HiloDemo");
System.out.println("Hilo hijo: " + t);
t.start(); //comienza el hilo
}
public void run(){
try{
for(int i = 5; i > 0; i--){
System.out.println("Hilo hijo :" + i);
Thread.sleep(500);
}
}catch(InterruptedException e){
System.out.println("Salida del hilo hijo");
}
}
}
class Hilo{
public static void main(String args[]){
new HiloNuevo();
try{
for(int i = 5; i > 0; i--){
System.out.println("Hilo principal: "+i);
Thread.sleep(1000);
}
}catch(InterruptedException e){
System.out.println("Se interrumpe el hilo principal");
}
System.out.println("Salida del hilo principal");
}
}
Resultado:
En este programa vemos que termina en último lugar el hilo principal, esto es porque lo altere para que así pasara, el hilo principal duerme más tiempo que el hilo hijo. En lo personal yo prefiero crear hilos implementando la interfaz Runnable me parece más fácil.
Creación de múltiples hilos
En los ejemplos anteriores solo había creado un hilo, sin embargo podemos crear tantos hijos como queramos. En el siguiente programa creo 3 hijos.
//creo 3 hijos
class HiloNuevo implements Runnable{
String nombre;
Thread t;
HiloNuevo(String nombreHilo){
nombre = nombreHilo;
t = new Thread(this, nombre);
System.out.println("Hilo nuevo "+ t);
t.start();
}
public void run(){
try{
for(int i=5; i>0;i--){
System.out.println(nombre +":" + i);
Thread.sleep(500);
}
}catch(InterruptedException e){
System.out.println("Interrupción del hilo" + nombre);
}
System.out.println("salida del hilo" + nombre);
}
}
class Hilos{
public static void main(String args[]){
new HiloNuevo("uno");
new HiloNuevo("dos");
new HiloNuevo("tres");
try{
//ESPERAMOS A QUE LOS OTROS HILOS TERMINEN
Thread.sleep(10000);
}catch(InterruptedException e){
System.out.println("Interrupcion del hilo principal");
}
System.out.println("Salida del hilo principal");
}
}
Resultado:
Uso de isAlive() y join()
Como hemos visto en nuestros programas le decimos al hilo principal que duerma más tiempo que los demás para que no termine antes, pero esto no es muy conveniente ya que nosotros suponemos que en ese lapso de tiempo terminaran los demás, pero y si no es así?, para eso tenemos el método isAlive() que es un método definido por la clase Thread y su forma general es la siguiente:
final boolean isAlive().
Este método devuelve el valor True si el hilo al que se hace referencia aún se esta ejecutando, y devuelve false en caso contrario.
Ese método es útil en algunos casos, pero hay otro método más común join(), que se utiliza para esperar a que un hilo termine. Su forma general es:
final void join() throws InterruptedException
Ese método espera a que termine el hilo sobre el que se realizo la llamada. Su nombre viene de que el hilo llamante espera a que el hilo especificado se reuna con el. Otras formas de join permiten especificar el máximo tiempo de espera para que termine el hilo.
Ejemplo de isAlive y join()
//usamos jin para esperar a que terminen
class NuevoHilo implements Runnable{
String nombre;
Thread t;
NuevoHilo(String nom){
nombre = nom;
t = new Thread(this, nombre);
System.out.println("Nuevo hilo: "+ t);
t.start();
}
//aqui entra el hilo
public void run(){
try{
for(int i=5; i>0; i--){
System.out.println(nombre + ": " +i);
Thread.sleep(1000);
}
}catch(InterruptedException e){
System.out.println("Interrupcion del hilo " + nombre);
}
System.out.println("Sale el hilo "+ nombre);
}
}
class DemoJoin{
public static void main (String args[]){
NuevoHilo ob1 = new NuevoHilo("uno");
NuevoHilo ob2 = new NuevoHilo("dos");
NuevoHilo ob3 = new NuevoHilo("tres");
System.out.println("El hilo uno esta vivo " + ob1.t.isAlive());
System.out.println("El hilo dos esta vivo " + ob2.t.isAlive());
System.out.println("El hilo tres esta vivo " + ob3.t.isAlive());
//espera a que los otros hilos terminen
try{
System.out.println("Espera a que finalizen los otros hilos");
ob1.t.join();
ob2.t.join();
ob3.t.join();
}catch(InterruptedException e){
System.out.println("El hilo uno esta vivo " + ob1.t.isAlive());
System.out.println("El hilo dos esta vivo " + ob2.t.isAlive());
System.out.println("El hilo tres esta vivo " + ob3.t.isAlive());
System.out.println("El hilo principal termina");
}
}
}
Resultado
Prioridades de los hilos
El planificador de hilos usa las prioridades de los hilos para poder decidir cuando se va a permitir la ejecución de cada hilo. Los hilos que tienen prioridad más alta tienen mayor tiempo del CPU que los de prioridad baja. Un hilo de prioridad alta puede desalojar a uno de prioridad más baja. Por ejemplo, cuando un hilo de prioridad mas baja se esta ejecutando y otro de prioridad más alta reanuda su ejecución, este segundo desalojará al de prioridad más baja.
Los hilos con la misma prioridad tienen el mismo acceso al CPU.
Para establecer la prioridad de un hilo se utiliza
setPriority(), que es miembro de la clase Thread,. Su forma general es:
final void setPriority(int nivel)
En donde nivel es la nueva prioridad del hilo. El valor del nivel debe de estar entre MIN_PRIORITY y MAX_PRIORITY. Actualmente estos valores son 1 y 10. Para establecer la prioridad por default que tienen los hilos de nuevo se usa NORM_PRIORITY, es 5.
Para obtener la prioridad de un hilo usamos getPriority() de Thread:
final int getPriority
Sincronización
En Java la sincronización parece sencilla ya que cada hilo tiene su propio monitor implícito asociado. Para entrar al monitor basta con llamar a un método modificado con la palabra clave synchronized. Mientras hay un hilo dentro de un método sincronizado, todos los demás hilos que tratan de llamar a ese método, o a otro método sincronizado sobre la misma instancia, tienen que esperar. Para salir del monitor simplemente hay que salir del método sincronizado.
La palabra clave synchronized, permite que un hilo acceda a un método sin interrupciones, aunque otros hilos quieran usarlo.
Comunicación entre hilos
- wait() Indica al hilo que realiza la llamada que debe abandonar al monitor y quedar suspendido hasta que algún otro hilo llame al método notify()
- notify() Activa a un hilo que previamente llamo a while en el mismo objeto.
- notify all() Activa todos los hilos que llamaron previamente a wait() en el mismo objeto. Uno de esos hilos empezar a ejecutarse.
Los métodos se llaman asi:
final void wait()
final void notify()
final void notifyAll()
Productor consumidor en Java
class Q{
int n;
synchronized int get(){
System.out.println("Consume: "+ n);
return n;
}
synchronized void put(int n){
this.n = n;
System.out.println("Produce: "+ n);
}
}
class Productor implements Runnable{
Q q;
Productor(Q q){
this.q = q;
new Thread(this, "Productor").start();
}
public void run(){
int i = 0;
while(true){
q.put(i++);
}
}
}
class Consumidor implements Runnable{
Q q;
Consumidor(Q q){
this.q = q;
new Thread(this, "Consumidor").start();
}
public void run(){
while(true){
q.get();
}
}
}
class PC{
public static void main(String args[]){
Q q = new Q();
new Productor(q);
new Consumidor(q);
System.out.println("control c para finalizar");
}
}
En ese programa aunque se sincronizan los métodos put y get nada impide que el productor vaya más rápido que el consumidor, ni que el consumidor recolecte el mismo valor de la cola dos veces. Entonces la implementación correcta del productor consumidor es usando
wait() y notify().
En el siguiente código muestro como hacerlo:
//productor consumidor simple
class Q{
int n;
boolean valueSet = false;
synchronized int get(){
while(!valueSet)
try{
wait();
}catch(InterruptedException e){
System.out.println("captura de la excepcion");
}
System.out.println("Consume: " + n);
valueSet = false;
notify();
return n;
}
synchronized void put(int n){
while(valueSet)
try{
wait();
}catch(InterruptedException e){
System.out.println("Captura de la excepcion");
}
this.n = n;
valueSet = true;
System.out.println("Produce: "+ n);
notify();
}
}
class Productor implements Runnable{
Q q;
Productor(Q q){
this.q = q;
new Thread(this, "Productor").start();
}
public void run(){
int i = 0;
while(true){
q.put(i++);
}
}
}
class Consumidor implements Runnable{
Q q;
Consumidor(Q q){
this.q = q;
new Thread(this, "Consumidor").start();
}
public void run(){
while(true){
q.get();
}
}
}
class PC{
public static void main(String args[]){
Q q = new Q();
new Productor(q);
new Consumidor(q);
System.out.println("control c para finalizar");
}
}
Resultado:
Finalmente, he aprendido a hacer programas multitarea en Java ahora podré aportar al equipo, si así se requiere programación en Java también :) .