Programación Concurrente con Java

Modesto Tomás Saavedra  

  1. Conceptos Básicos Java
  2. Conceptos Básicos Sobre Hilos
  3. Clases Relacionadas con los Hilos
  4. Creación de Hilos
  5. Estado y Control de un Hilo
  6. Planificación y Prioridad de Hilos
  7. Sincronización
  8. Hilos Daemon
  9. Conclusiones
  10. Bibliografía

1. Java

Cuando se escribe un programa, en la mayoría de los lenguajes de programación, es necesario decidir el procesador y sistema operativo en los que se va a ejecutar, porque éstos lenguajes incluyen llamadas a funciones específicas de una biblioteca asociada al sistema operativo de la plataforma destino. Cuando se está preparado para probar el programa, se envía el código fuente a un compilador que lo transforma en un conjunto de instrucciones propias de la plataforma destino. Por ejemplo, Windows se ejecuta generalmente en un procesador Intel, como un Pentiunm, mientras que los Macintosh utilizan procesadores Motorola 68000 o PowerPC.

Cuando se escribe en Java, no necesitamos pensar en llamadas a Windows, Mac OS, u otras bibliotecas del sistema operativo. Java tiene sus propias bibliotecas, llamadas paquetes, que son independientes de la plataforma. Por ello no es necesario preocuparse si la aplicación se va a ejecutar en una plataforma Intel, una PowerPC o una SPARC. El compilador de Java no genera instrucciones nativas, en su lugar genera los llamados "código de byte" (byte code) para la Máquina Virtual Java (Java Virtual Machine o JVM), que es una máquina que no existe físicamente.

Sun Microsystems (creadores de Java) y otras empresas han desarrollado versiones software de la JVM para una gran parte de las plataformas existentes en el mercado, es decir cada plataforma tiene su propia máquina virtual de Java, y es ésta la que ejecuta los byte code. En Java podemos distinguir dos tipos de programas, las aplicaciones autosuficientes que son conocidas como aplicaciones y los que se ejecutan con la ayuda de otro programa (un navegador Web), que se conocen como applets. Las características principales que nos ofrece Java respecto a cualquier otro lenguaje de programación son:

Java es simple

Java ofrece toda la funcionalidad de un lenguaje potente, pero sin las características menos usadas y más confusas de éstos. C++ es un lenguaje que adolece de falta de seguridad, pero C y C++ son lenguajes más difundidos, por ello Java se diseñó para ser parecido a C++ y así facilitar un rápido y fácil aprendizaje.

Java elimina muchas de las características de otros lenguajes como C++, para mantener reducidas las especificaciones del lenguaje y añadir características muy útiles como el garbage collector (recolector de memoria dinámica). No es necesario preocuparse de liberar memoria, el recolector se encarga de ello y como es un thread (hilo de ejecución) de baja prioridad, cuando entra en acción, permite liberar bloques de memoria muy grandes, lo que reduce la fragmentación de la memoria.

Java reduce en un 50% los errores más comunes de programación con lenguajes como C y C++ al eliminar muchas de las características de éstos, entre las que destacan:

Java es orientado a objetos

Java implementa la tecnología básica de C++ con algunas mejoras y elimina algunas cosas para mantener el objetivo de la simplicidad del lenguaje. Java trabaja con sus datos como objetos. Soporta las tres características propias del paradigma de la orientación a objetos: encapsulación, herencia y polimorfismo. Las plantillas de objetos son llamadas, como en C++, clases y sus copias, instancias. Estas instancias, como en C++, necesitan ser construidas y destruidas en espacios de memoria.

Java es distribuido

Java se ha construido con extensas capacidades de interconexión TCP/IP. Existen librerías de rutinas para acceder e interactuar con protocolos como http y ftp. Esto permite a los programadores acceder a la información a través de la red con tanta facilidad como a los ficheros locales.

La verdad es que Java en sí no es distribuido, sino que proporciona las librerías y herramientas para que los programas puedan ser distribuidos, es decir, que se ejecuten en vanas máquinas, interactuando.

Java es multiplataforma

Para establecer Java como parte integral de la red, el compilador Java compila su código a un fichero objeto de formato independiente de la arquitectura de la máquina en que se ejecutará. Cualquier máquina que tenga el sistema de ejecución (run-time) puede ejecutar ese código objeto, sin importar en modo alguno la máquina en que ha sido generado. Actualmente existen sistemas run time para Solaras 2.x, SunOs 4.1.x, Windows 95/98, Windows NT, Linux, Irix, Aix, Mac, Apple y probablemente haya grupos de desarrollo trabajando en el paso a otras plataformas.

El código fuente Java se "compila" a un código de bytes de bajo nivel independiente de la máquina. Este código (byte code) está diseñado para ejecutarse en una máquina hipotética que es implementada por un sistema run-time, que sí es dependiente de la máquina.

Java es robusto

Java realiza verificaciones en busca de problemas tanto en tiempo de compilación como en tiempo de ejecución. La comprobación de tipos en Java ayuda a detectar errores, lo antes posible, en el ciclo de desarrollo. Java obliga a la declaración explícita de métodos, reduciendo así las posibilidades de error. Maneja la memoria para eliminar las preocupaciones por parte del programador de la liberación o corrupción de memoria. También implementa los arrays auténticos, en vez de listas enlazadas de punteros, con comprobación de límites, para evitar la posibilidad de sobreescribir o corromper memoria resultado de punteros que señalan a zonas equivocadas. Estas características reducen drásticamente el tiempo de desarrollo de aplicaciones en Java.

Java es seguro

La seguridad en Java tiene dos facetas. En el lenguaje, características como los punteros o el casting implícito que hacen los compiladores de C y C++ se eliminan para prevenir el acceso ilegal a la memoria. Otra laguna de seguridad u otro tipo de ataque, es el Caballo de Troya. Se presenta un programa como una utilidad, resultando tener una funcionalidad destructiva. El código Java pasa muchos tests antes de ejecutarse en una máquina. El código se pasa a través de un verificador de byte codes que comprueba el formato de los fragmentos de código y aplica un probador de teoremas para detectar fragmentas de código ilegal, código que falsea punteros, viola derechos de acceso sobre objetos o intenta cambiar el tipo o clase de un objeto. Si los byte codes pasan la verificación sin generar ningún mensaje de error, entonces sabemos que: El código no produce desbordamiento de operandos en la pila.

El Cargador de Clases también ayuda a Java a mantener su seguridad, separando el espacio de nombres del sistema de ficheros local, del de los recursos procedentes de la red. Esto limita cualquier aplicación del tipo Caballo de Troya, ya que las clases se buscan primero entre las locales y luego entre las procedentes del exterior.

Las clases importadas de la red se almacenan en un espacio de nombres privado, asociado con el origen. Cuando una clase del espacio de nombres privado accede a otra clase, primero se busca en las clases predefinidas (del sistema local) y luego en el espacio de nombres de la clase que hace la referencia. Esto imposibilita que una clase suplante a una predefinida.

En resumen, las aplicaciones de Java resultan extremadamente seguras, ya que no acceden a zonas delicadas de memoria o de sistema, con lo cual evitan la interacción de ciertos virus. Java no posee una semántica específica para modificar la pila de programa, la memoria libre o utilizar objetos y métodos de un programa sin los privilegios del núcleo del sistema operativo. Además, para evitar modificaciones por parte de los crackers de la red, implementa un método seguro de autentificación por clave pública. El Cargador de clases puede verificar una firma digital antes de realizar una instancia de un objeto. Por tanto, ningún objeto se crea y almacena en memoria, sin que se validen los privilegios de acceso. Es decir, la seguridad se integra en el momento de compilación, con el nivel de detalle y de privilegio que sea necesario.

Dada, pues la concepción del lenguaje y si todos los elementos se mantienen dentro del estándar marcado por Sun, no hay peligro. Java imposibilita, también, abrir ningún fichero de la máquina local (siempre que se realizan operaciones con archivos, éstas trabajan sobre el disco duro de la máquina de donde partió el applet), no permite ejecutar ninguna aplicación nativa de _una plataforma (desde un applet) e impide que se utilicen otros ordenadores como puente, es decir, nadie puede utilizar nuestra máquina para hacer peticiones o realizar operaciones con otra­Además, los intérpretes que incorporan los navegadores de la Web son aún más restrictivos,

Java es interpretado

El intérprete Java puede ejecutar directamente el código objeto. Enlazar (linkar) un programa, normalmente, consume menos recursos que compilarlo, por lo que los desarrolladores con Java pasarán más tiempo desarrollando y menos esperando. No obstante, el compilador actual del JDK (Java Development Kit) es bastante lento. Por ahora, que todavía no hay compiladores específicos de Java para las diversas plataformas, Java es más lento que otros lenguajes de programación, como C++, ya que debe ser interpretado y no ejecutado como sucede en cualquier programa tradicional.

Se dice que Java es de 10 a 30 veces más lento que C, y que tampoco existen en Java proyectos de gran envergadura como en otros lenguajes. Lo que hay que dejar claro en todo esto, es que primero habría que decidir hasta que punto Java, un lenguaje en pleno desarrollo y todavía sin definición definitiva, está maduro corno lenguaje de programación para ser comparado con otros; como por ejemplo con Smalltalk, que lleva más de 20 años.

La verdad es que Java para conseguir ser un lenguaje independiente del sistema operativo y del procesador que incorpore la máquina utilizada, es tanto interpretado como compilado. Y esto no es ningún contrasentido, el código fuente escrito con cualquier editor se compila generando el byte code. Este código intermedio es de muy bajo nivel, pero sin alcanzar las instrucciones máquina propias de cada plataforma. El byte code corresponde al 80% de las instrucciones de la aplicación. Ese mismo código es el que se puede ejecutar sobre cualquier plataforma. Para ello hace falta el run time, que sí es completamente dependiente de la máquina y del sistema operativo, que interpreta dinámicamente el byte code y añade el 20% de instrucciones que faltaban para su ejecución. Con este sistema es fácil crear aplicaciones multiplataforma, pero para ejecutarlas es necesario que exista el run time correspondiente al sistema operativo utilizado.

Java es multithreaded

Al ser multithreaded (multi-hilo), Java permite muchas actividades simultáneas en un programa. Los threads, son básicamente pequeños procesos o piezas independientes de un gran proceso. Al estar los threads incluidos en el lenguaje, son más fáciles de usar y más robustos que las sus implementaciones en C o C++.

El beneficio de ser miltithreaded consiste en un mejor rendimiento interactivo y mejor comportamiento en tiempo real.

Aunque el comportamiento en tiempo real está limitado a las capacidades del sistema operativo sobre el que corre, aún supera a los entomos de flujo único de programa (single-threaded) tanto en facilidad de desarrollo como en rendimiento. A continuación profundizaremos un poco más en el concepto de threads.

2. Conceptos Básicos sobre Hilos

El multihilo soportado en Java gira alrededor del concepto de hilo. La cuestión es, ¿qué es un hilo? De forma sencilla, un hilo es un único flujo de ejecución dentro de un proceso. Pero será mejor comenzar desde el principio, un proceso es un programa ejecutándose dentro de su propio espacio de direcciones. lava es un sistema multiproceso, esto significa que soporta varios procesos corriendo a la vez dentro de sus propios espacios de direcciones. Estamos más familiarizados con el término multitarea, el cual describe un escenario muy similar al multiproceso.

Por ejemplo, consideremos la cantidad de aplicaciones que corren a la vez dentro de un mismo entorno gráfico. Mientras escribo esto, está corriendo Microsoft Word además de Internet Explorer, Windows Explorer, CD Player y el Volumen Control. Estas aplicaciones son todas procesos ejecutados dentro de Windows 98. De esta forma, se puede pensar que los procesos son análogos a las aplicaciones o a programas aislados, pero cada proceso tiene asignado espacio propio de ejecución dentro del sistema.

Un hilo es una secuencia de código en ejecución dentro del contexto de un proceso. Los hilos no pueden ejecutarse ellos solos; requieren la supervisión de un proceso padre para correr.Dentro de cada proceso hay varios hilos ejecutándose. Por ejemplo, Word puede tener un hilo en background chequeando automáticamente la gramática de lo que estoy escribiendo, mientras otro hilo puede estar salvando automáticamente los cambios del documento en el que estoy trabajando. Como Word, cada aplicación (proceso) puede correr varios hilos los cuales están realizando diferentes tareas. Esto significa que los hilos están siempre asociados con un proceso en particular.

Los hilos a menudo son conocidos o llamados procesos ligeros. Un hilo, en efecto, es muy similar a un proceso pero con la diferencia de que un hilo siempre corre dentro del contexto de otro programa. Por el contrario, los procesos mantienen su propio espacio de direcciones y entorno de operaciones. Los hilos dependen de un programa padre en lo que se refiere a recursos de ejecución

Java es un lenguaje de programación que incorpora hilos en el corazón del mismo lenguaje. Comúnmente, los hilos son implementados a nivel de sistema, requiriendo una interfaz de programación específica separada del núcleo del lenguaje de programación. Esto es lo que ocurre con CIC++ programando en Windows, porque se necesita usar la interfaz de programación Win32 para desarrollar aplicaciones Windows multihilo.

Java se presenta como ambos, como lenguaje y como sistema de tiempo de ejecución (runtime), siendo posible integrar hilos dentro de ambos. El resultado final es que se pueden usar hilos Java como standard, en cualquier plataforma.

3. Clases Relacionadas con los Hilos

El lenguaje de programación Java proporciona soporte para hilos a través de una simple interfaz y un conjunto de clases. La interfaz de Java y las clases que incluyen funcionalidades sobre hilos son las siguientes:

Thread

La clase Thread es la clase responsable de producir hilos funcionales para otras clases. Para añadir la funcionalidad de hilo a una clase simplemente se deriva la clase de Thread y se ignora el método run. Es en este método run donde el procesamiento de un hilo toma lugar, ya menudo se refieren a él como el cuerpo del hilo. La clase Thread también define los métodos start y stop, los cuales te permiten comenzar y parar la ejecución del hilo, además de un gran número de métodos útiles.

Runnable

Java no soporta herencia múltiple de forma directa, es decir, no se puede derivar una clase de varias clases padre. Esto nos plantea la duda sobre cómo podemos añadir la funcionalidad de Hilo a una clase que deriva de otra clase, siendo ésta distinta de Thread. Para lograr esto se utiliza la interfaz- Runnable. La interfaz Runnable proporciona la capacidad de añadir la funcionalidad de un hio a una clase simplemente implementando la interfaz, en lugar de derivándola de la clase Thread.

Las clases que implementan la interfaz Runnable proporcionan un método run que es ejecutado por un objeto hilo asociado que es creado aparte. Esta es una herramienta muy útil y a menudo es la única salida que tenemos para incorporar multihilo dentro de las clases. Esta cuestión será tratada más ampliamente en el apartado de Creación de hilos.

Object

Aunque, estrictamente hablando, no es una clase de apoyo a los hilos, la clase objeto proporciona unos cuantos métodos cruciales dentro de la arquitectura multihilo de Java. Estos métodos son wait, notify y notifyAll. El método wait hace que el hilo de ejecución espere en estado dormido hasta que se le notifique que continúe. Del mismo modo, el método notify informa a un hilo en espera de que continúe con su ejecución. El método notifyAll es similar a notify excepto que se aplica a todos los hilos en espera. Estos tres métodos solo pueden ser llamados desde un método o bloque sincronizado (o bloque de sincronización).

4. Creación de Hilos

En Java, los hilos comparten el mismo espacio de memoria. Incluso comparten gran parte del entorno de ejecución, de modo que la creación de nuevos hilos es mucho más rápida que la creación de nuevos procesos. La ventaja que proporcionan los hilos es la capacidad de tener más de un camino de ejecución en un mismo programa. Así, con un único proceso, ejecutándose una J VM (Java Virtual Machine), habrá siempre más de un hilo, cada uno con su propio camino de ejecución.

En cuanto al proceso de creación de hilos, son dos los mecanismos que nos permiten llevarlo a cabo en Java: implementando la interfaz Runnable, o extendiendo la clase Thread, esto es, creando una subclase de esta clase.

En ambos casos, se deber definir un método run que será el incluya las instrucciones que se ejecutarán en el thread y se pueden definir prioridades aunque no se puede confiar en que la maquina virtual escoja para ejecutar,- siempre, el de mayor prioridad por lo que no se pueden utilizar para basar en ellas el scheduler e un sistema de tiempo real

Creación de un hilo: la clase Thread

class Repeticion extends Thread {

	prívate int repeticiones;
	private String mensaje;
	Repeticion (String msg, int n) {
		mensaje = msg; repeticiones = n;
		}

public void run () {

	for (int i = 1; i <= repeticiones; i++)
	System.out.println (mensaje + " " + i);
	}

public static void main (String args[]) {

	Repeticion r1 = new Repeticion ("Rojo", 5);
	Repeticion r2 = new Repeticion ("Azul", 80);
	rl.start ();
	r2.start ();
	}

En el caso de crear un hilo extendiendo la clase Thread, se pueden heredar los métodos y variables de la clase padre. Si es así, una misma subclase solamente puede extender o derivar una vez de la clase padre Thread. Esta limitación de Java puede ser superada a través de la implementación de Runnable.

Creación de un hilo: la interfaz Runnable

class Repeticion2 implements Runnable {

	private int repeticiones;
	private String mensaje;
	Repeticion2 (String msg, int n) {
		mensaje = msg; repeticiones = n;
		}

public void run () {
	for (int i = 1; i <= repeticiones; i++)
		System.out.println (mensaje + " " + i);
	}

}

public static void main (String args[]) {
	Repeticion rl = new Repeticion ("Rojo", 5);
	Thread r2 = new Thread (new Repeticion2 ("Azul", 80));
	r1. start ();
	r2. start ();
}

5. Estado y Control de Hilos

5.a. Estados de un Hilo

El comportamiento de un hilo depende del estado en que se encuentre, este estado define su modo de operación actual, por ejemplo, si esta corriendo o no. A continuación proporcionamos la relación de estados en los que puede estar un hilo Java.

New

Un hilo esta en el estado new la primera vez que se crea y hasta que el método start es llamado. Los hilos en estado new ya han sido inicializados y están listos para empezar a trabajar, pero aún no han sido notificados para que empiecen a realizar su trabajo.

Runnable

Cuando se llama al método start de un hilo nuevo, el método run es invocado y el hilo entra en el estado runnable. Este estado podría llamarse "running" porque la ejecución del método run significa que el hilo esta corriendo. Sin embargo, debemos tener en cuenta la prioridad de los hilos. Aunque cada hilo está corriendo desde el punto de vista del usuario, en realidad todos los hilos, excepto el que en estos momentos esta utilizando la CPU, están en el estado runnable (ejecutables, listos para correr) en cualquier momento dado. Uno puede pensar conceptualmente en el estado runnable como si fuera el estado "numing", sólo tenemos que recordar que todos los hilos tienen que compartir los recursos del sistema.

Bloked

El estado not running se aplica a todos los hilos que están parados por alguna razón. Cuando un hilo está en este estado, está listo para ser usado y es capaz de volver al estado runnable en un momento dado. Los hilos pueden pasar al estado not running a través de varias vías.

A continuación se citan diferentes eventos que pueden hacer que un hilo esté parado de modo temporal.

Para cada una de estas acciones que implica que el hilo pase al estado not running hay una forma para hacer que el hilo vuelva a correr. A continuación presentamos la lista de eventos correspondientes que pueden hacer que el hilo pase al estado runnable.

..SI un hilo está suspendido, la invocación del método resume

..Si un hilo está durmiendo, pasarán el número de milisegundos que se ha especificado que debe dormir

., Si un hilo está esperando, la llamada a notify o notifyAll por parte del objeto por el que espera „Si un hilo está bloqueado por UO, la finalización de la operación 1/O en cuestión

Dead

Un hilo entra en estado dead cuando ya no es un objeto necesario. Los hilos en estado dead no pueden ser resucitados y ejecutados de nuevo. Un hilo puede entrar en estado dead a través de dos vías:

..El método run termina su ejecución.

..El método stop es llamado.

La primera opción es el modo natural de que un hilo muera. Uno puede pensar en la muerte de un hilo cuando su método run termina la ejecución como una muerte por causas naturales.

En contraste a esto, está la muerte de un hilo "por causa" de su método stop. Una llamada al método stop mata al hilo de modo asíncrono.

Aunque la segunda opción suene un poco brusca, a menudo es muy útil. Por ejemplo, es bastante común que los applets maten sus hilos utilizando el método stop cuando el propio método stop del applet ha sido invocado. La razón de esto es que el método stop del applet es llamado normalmente como respuesta al hecho de que el usuario ha abandonado la página web que contenía el applet y no es adecuado dejar hilos de un applet corriendo cuando el applet no está activo, así que es deseable matar los hilos.

En esta figura podemos ver gráficamente los diferentes estados por los que pude pasar un hilo:

5.b. Control de un hilo Atranque de un hilo

En el contexto de las aplicaciones, sabemos que es main la primera función que se invoca tras arrancar, y por tanto, lógicamente, es el lugar más apropiado para crear y arrancar otros hilos.

La línea de código:

tl = new TestTh( "Thread 1",(int)(Math.randomo*2000) );

Siendo TestTh una subclase de la clase Thread (o una clase que implemente la interfaz Runnable) crea un nuevo hilo. Los dos argumentos pasados, sin mayor relevancia, satisfarán el prototipo del constructor de la clase y se utilizarán para la inicialización del objeto.

Al tener control directo sobre los hilos, tenemos que arrancarlos explícitamente. Como ya se comentó anteriormente, es la función miembro start la que nos permite hacerlo. En nuestro ejemplo sería:

tl.startO;

start, en realidad es un método oculto en el hilo que llama al método run.

Manipulación de un hilo

Si todo fue bien en la creación del objeto TestTh (tl), éste debería contener un hilo, una traza de ejecución válida, que controlaremos en el método run del objeto.

El cuerpo de esta función miembro viene a ser el cuerpo de un programa como ya los conocemos. Digamos que es la rutina main a nivel de hilo. Todo lo que queremos que haga el hilo debe estar dentro del método run. Cuando finalice run, finalizará también el hilo que lo ejecutaba.

Suspensión de un Hilo

La función miembro suspend de la clase Thread permite tener un control sobre el hilo de modo que podamos desactivarlo, detener su actividad durante un intervalo de tiempo indeterminado, a diferencia del uso de la llamada al sistema sleep, que simplemente lleva al hilo a un estado de "dormido", y siempre durante un número de milisegundos concreto.

Este método puede resultar útil si, construyendo un applet con un hilo de animación, queremos permitir al usuario detener (que no finalizar) la animación, hasta que éste decida reanudarla.

Este método no detiene la ejecución permanentemente. El hilo es suspendido indefinidamente y para volver a activarlo de nuevo necesitamos realizar una invocación a la función miembro resume. 12

Parada de un Hilo

Ya conocemos los métodos de control de hilos que nos permiten arrancarlos, suspenderlos y reanudarlos. El último elemento de control que se necesita sobre hilos es el método stop, utilizado para terminar la ejecución de un hilo de forma permanente: tl.stopo;

Señalar que esta llamada no destruye el hilo, sino que detiene su ejecución, y ésta no puede reanudarse con el método start. Cuando se desasignen las variables que se usan en el hilo, el objeto hilo (creado con new) quedará marcado para eliminarlo y el garbage collector (recolector de basura de Java) se encargará de liberar la memoria que utilizaba.

Tiene sentido su utilidad, por ejemplo, en aplicaciones complejas que necesiten un control sobre cada uno de los hilos que se lancen.

Por último, un método de control de hilos que nos permite comprobar si una instancia está viva (el hilo se ha arrancado y aún no se ha detenido) o no (bien no se arrancó; bien ya finalizó). Estamos hablando de la función miembro isAlive. tl.isAliveo;

Devolverá true en caso de que el hilo tl esté vivo, es decir, ya se haya llamado a su método run y no haya sido parado con un stop ni haya terminado el método run en su ejecución. En otro caso, lógicamente, devolverá false.

6. Planificación y Prioridad de Hilos

6.a. Planificación (Scheduling)

Java tiene un Planificador (Scheduler), una lista de procesos, que muestra por pantalla todos los hilos que se están ejecutando en todos los programas y decide cuáles deben ejecutarse y cuáles deben encontrarse preparados para su ejecución. Hay dos características de los hilos que el planificador tiene en cuenta en este proceso de decisión.

..La prioridad del hilo (la más importante).

..El indicador de demonio (que pasaremos a explicar en los siguientes apartados).

La regla básica del planificador es que si solamente hay hilos demonio ejecutándose, la Máquina Virtual Java (JVM) concluirá. Los nuevos hilos heredan la prioridad y el indicador de demonio de los hilos que los han creado. El planificador determina qué hilos deberán ejecutarse comprobando la prioridad de todos los hilos. Aquellos con prioridad más alta dispondrán del procesador antes de los que tienen prioridad más baja.

El planificador puede seguir dos patrones, preventivo y no preventivo. Los planificadores preventivos proporcionan un segmento de tiempo a todos los hilos que están corriendo en el sistema. El planificador decide cuál será el siguiente hilo a ejecutarse y llama a resume para darle vida durante un período fijo de tiempo.

Cuando finaliza ese período de tiempo, se llama a su método suspend y el siguiente hilo en la lista de procesos será relanzado mediante su método resume. Los planificadores no preventivos, en cambio, deciden qué hilo debe correr y lo ejecutan hasta que concluye. El hilo tiene control total sobre el sistema mientras esté en ejecución. El método yield es un mecanismo que permite a un hilo forzar al planificador para que comience la ejecución de otro hilo que esté esperando. Dependiendo del sistema en que esté corriendo Java, el planificador será preventivo o no preventivo.

El planificador de hilos no está especificado tan rigurosamente como el resto de clases en Java, y en este asunto hay, sin duda, diferencias de un sistema a otro. Por ejemplo, la versión del JDK 1.0.2 de Solafs es una versión no preventiva, a diferencia del planificador de Win32, que sí lo es. [Esto es exactamente lo opuesto a lo que nos esperábamos.]

Un planificador no preventivo no interrumpirá un hilo en ejecución, de forma muy parecida al comportamiento de Windows 3.1. El planificador de hilos Java de Win32 sí interrumpirá los hilos en ejecución, dando lugar a una planificación más fiable. Por ejemplo, si se arrancan dos hilos con grandes bucles de ejecución bajo un sistema Solaris, el hilo que arrancó antes completará su tarea antes de que el otro consiga arrancar. Pero en Windows95 o NT, el segundo hilo sí consigue turno de ejecución.

6.b. Prioridad

Cada hilo tiene una prioridad, que no es más que un valor entero entre 1 y 10, de modo que cuanto mayor el valor, mayor es la prioridad.

El planificador determina el hilo que debe ejecutarse en función de la prioridad asignad, a cada uno de ellos. Cuando se crea un hilo en Java, éste hereda la prioridad de su padre, el hilo que lo ha creado. A partir de aquí se le puede modificar su prioridad en cualquier momento utilizando el método setPriority . Las prioridades de un hilo varían en un rango de entero: comprendido entre MIN_PRIORITY y MAX_PRIORITY (anbas definidas en la clase Thread) El entero más alto designará la prioridad más alta y el más bajo, como es de esperar, la menor Se ejecutará primero el hilo de prioridad superior, el llamado "Ejecutables", y sólo cuando éste para, abandona o se convierte en "No Ejecutable", comienza la ejecución de en hilo de prioridad inferior. Si dos hilos tienen la misma prioridad, el programador elige uno de ello! en alguna forma de competición. El hilo seleccionado se ejecutará hasta que:

..Un hilo comprioridad mayor pase a ser "Ejecutable".

* En sistemas que soportan tiempo-compartido, termina su tiempo. ..Abandone, o termine su método run.

Luego, un segundo hilo puede ejecutarse, y así continuamente hasta que el intérprete abandone.

El algoritmo del sistema de ejecución de hilos que sigue Java es de tipo preventivo. S en un momento dado un hilo que tiene una prioridad mayor a cualquier otro hilo que se est; ejecutando pasa a ser "Ejecutable", entonces el sistema elige a este nuevo hilo.

7. Sincronización

El problema de la sincronización de hilos tiene lugar cuando varios hilos intentan acceder al mismo recurso o dato. A la hora de acceder a datos comunes, los hilos necesitan establecer cierto orden, por ejemplo en el caso del productor consumidor. Para asegurarse de que hilos concurrentes no se estorban y operan correctamente con datos (o recursos) compartidos, un sistema estable previene la inanición y el punto muerto o interbloqueo. La inanición tiene lugar cuando uno o más hilos están bloqueados al intentar conseguir acceso a un recurso compartido de ocurrencias limitadas. El interbloqueo es la última fase de la inanición; ocurre cuando uno o más hilos están esperando una condición que no puede ser satisfecha. Esto ocurre muy frecuentemente cuando dos o más hilos están esperando a que el otro u otros se desbloquee, respectivamente.

A continuación se presenta un ejemplo, el problema del Productor/Consumidor, con la intención de explicar de una forma más práctica el concepto y las situciones de sincronización de hilos.

7.a. Ejemplo: problema del productor-consumidor

El productor genera un entero entre 0 y 9 (inclusive), lo almacena en un objeto "CubbyHole", e imprime el número generado. Para hacer más interesante el problema de la sincronización, el productor duerme durante un tiempo aleatorio entre 0 y 100 milisegundos antes de repetir el ciclo de generación de números.

class Productor extends Thread {

	private CubbyHole cubbyhole;
	private int numero;

		public Productor(CubbyHole c, int numero) {
			cubbyhole = c;
			this.numero = numero;
			}

		public void run() {
			for (int i = 0; i < 10; i++) {
				cubbyhole.put(i);
				System.out.println("Productor#"+this.numero+"pone:"+i);
					try {
						sleep((int)(Math.randomo * 100));
					} catch (InterruptedException e) {
					  }
				}
		}
}

El consumidor, por su parte, está "hambriento", consume todos los enteros de CubbyHole (exactamente el mismo objeto en que el productor puso los enteros en primer lugar) tan pronto como estén disponibles. 16

class Consumidor extends Thread 
	prívate CubbyHole cubbyhole;
	private int numero;

public Consumidor(CubbyHole c, int numero) {
	cubbyhole = c;
	this.numero = numero;
	}

public void runo {
	int value = 0;
	for (int i = 0; i < 10; i++) {
		value = cubbyhole.geto;
		System.out.println("Consumidor#"+this.numero+"obtiene: "+value);
	}
}

En este ejemplo, el Productor y el Consumidor comparten datos a través de un objeto CubbyHole común. Observará que ninguno de los dos hace ningún tipo de esfuerzo para asegurarse de que el consumidor obtiene cada valor producido una y sólo una vez. La sincronización entre estos dos hilos realmente ocurre a un nivel inferior, dentro de los métodos geto y puto del objeto CubbyHole. Sin embargo, asumamos por un momento que estos dos hilos no están sincronizados y veamos los problemas potenciales que podría provocar esta situación.

Un problema sería el que se daría cuando el Productor fuera más rápido que el Consumidor y generara dos números antes de que el Consumidor tuviera una posibilidad de consumir el primer número. Así el Consumidor se saltaría un número. Parte de la salida se podría parecer a esto.

Consumidor #1 obtiene: 3
Productor #1 pone: 4
Productor #1 pone: 5
Consumidor #1 obtiene: 5

Otro problema podría aparecer si el consumidor fuera más rápido que el productor y consumiera el mismo valor dos o más veces. En esta situación el Consumidor imprimirá el mismo valor dos veces y podría producir una salida como esta.

Productor #1 pone: 4
Consumidor #1 obtiene: 4
Consumidor #1 obtiene: 4
Productor #1 pone: 5

De cualquier forma, el resultado es erróneo. Se quiere que el consumidor obtenga cada entero producido por el productor y sólo una vez. Los problemas como los descritos anteriormente, se llaman "condiciones de carrera". Se alcanzan cuando varios hilos ejecutados asíncronamente intentan acceder a un mismo objeto al mismo tiempo y obtienen resultados erróneos.

Para prevenir estas condiciones en nuestro ejemplo Productor/Consumidor, el almacenamiento de un nuevo entero en CubbyHole por el Productor debe estar sincronizado con la recuperación del entero por parte del Consumidor. El Consumidor debe consumir cada entero exactamente una vez. El programa productor-consumidor utiliza dos mecanismos diferentes para sincronizar los hilos Productor y Consumidor; los monitores, y los métodos notifyo y wait.

7.b. Monitores

A los objetos como CubbyHole, a los que acceden varios hilos, son llamados "condiciones variables". Una de las formas de controlar el acceso a estas condiciones variables y de, por tanto, sincronizar los hilos, son los monitores. Las secciones críticas son los segmentos del código donde los hilos concurrentes acceden a las condiciones variables. Estas secciones, en Java, se marcan normalmente con la palabra reservada synchronized:

Synchronized int MiMetodoo;

Generalmente, las secciones críticas en los programas de Java son los métodos. Sin embargo el uso indiscriminado de synchronized viola los fundamentos de la programación objetuaL por lo que es mejor utilizar synelironized sólo a nivel de métodos. Java asocia un solo monitor a cada objeto que tiene un método sincronizado. En el ejemplo anterior del productorconstm-iidor tiene dos métodos de sincronización: puto, que cambia el valor de Obby~o y geto, para recuperar el valor actual. Este sería el código fuente del objeto (lubhpdlohn utilizando las técnicas de sincronización nuevas:

class CubbyHole {
	prívate ínt contents;
	private boolean avaílable = false;

	public synchronized int get() {
		whíle (avaílable == false) {
			try {
				wait();
			} catch (InterruptedException e) {
			}
		}
		available = false;
		notify();
		return contents;
}

public synchronized void put(int value) {
	while (available == true) {
		try {
			wait();
		} catch (InterruptedExceptíon e) {
		}
		contents value;
		available true;
		notify();
	}
}

La variable contents tiene el valor actual de CubbyHole y available indica si se puede recuperar o no el valor. Cuando available es verdadero, el productor aún no ha acabado de producir.

CubbyHole tiene dos métodos de sincronización, y Java proporciona un solo monitor para cada ejemplar de CubbyHole (incluyendo el compartido por el Productor y el Consumidor). Siempre que el control entra en un método sincronizado, el hilo que ha llamado al método adquiere el monitor del objeto al cual pertenece el método. Otros hilos no pueden llamar a un métalo sincronizado del mismo objeto mientras el monitor no sea liberado.

Cuando el Productor invoca el método puto de CubbyHole, adquiere el monitor del objeto CubbyHole y por lo tanto el Consumidor no podrá llamar a geto de CubbyHole y se quedará bloquedo (existe un método, waito, que libera temporalmente el monitor). De igual forma sucede cuando el Consumidor invoca geto.

public synchronized void put(int value) {
	// El productor adquiere el monitor
	while (available == true) {
		try {
			wait();
		} catch (InterruptedException e) {
		contents = value;
		available = true;
		notify(); // lo notifica al Productor
			// El productor libera el monitor
}

public synchronized int get() {
	// El consumidor adquiere el monitor
	while (available == false) {
		try {
			wait(); // espera que el Productor invoque a notify()
		} catch (InterruptedException e) {
		}
		available = false;
		notify();
		return contents;
			// el Consumidor libera el monitor
}

8. Hilos Demonio (Daemon)

Un proceso demonio es un proceso que debe ejecutarse continuamente en modo background (en segundo plano), y generalmente se diseña para responder a peticiones de otros procesos a través de la red. La palabra "daemon" (proveniente de la palabra griega "ghost') es propia de UNIR, pero no se utiliza de este mismo modo en Windows. En Windows NT, los demonios se denominan "servicios". Cuando los servicios atienden peticiones, se conocen como la parte "Servidor" de una arquitectura Cliente/Servidor.

Los hilos demonio también se llaman servicios, porque se ejecutan, normalmente, con prioridad baja y proporcionan un servicio básico a un programa o programas cuando la actividad de la máquina es reducida. Un ejemplo de hilo demonio que está ejecutándose continuamente es el recolector de basura (garbage conector). Este hilo, proporcionado por la Máquina Virtual Java, comprueba las variables de los programas a las que no se accede nunca y libera estos recursos; devolviéndolos al sistema.

Un hilo puede fijar su indicador de demonio pasando un valor trae al método setDaemono. Si se pasa false a este método, el hilo será devuelto por el sistema como un hilo de usuario. No obstante, esto último debe realizarse antes de que se arranque el hilo con el método startO.

9. Conclusiones

Se pueden usar hilos Java como standard, sin tener en cuenta la plataforma en la que vayan a ejecurtarse.

La clase Thread es la clase responsable de producir hilos funcionales para otras clases. La interfaz Runnable proporciona la capacidad de añadir la funcionalidad de un hilo a una clase en lugar de derivándola de la clase Tbread.

Para arrancar un hilo se llama a su método start el cual invoca al método run del propio hilo. Todo la tarea del hilo debe estar dentro del método run.

Para terminar la ejecución de un hilo de forma permanente se utiliza su método stop. La clase ThreadGroup es la implementación del concepto de grupo de hilos en Java. Java tiene un Planificador (Scheduler); el cual decide que hilos deben ejecutarse y cuales encontrarse preparados para su ejecución.

Cada hilo tiene una prioridad, que no es más que un valor entero entre 1 y 10, de modo que cuanto mayor el valor, mayor es la prioridad.

Bibliografa

[1] Bruce Eckel " Thinking in JAVA "

[2] David Arnow ; Gerald Weiss " Introduccion a la programación con Java"

Referencias

[3] http://java.sun.com/docs/books/tutoriaVessential/threads

[4] http://java.sun.comldocs/books/tutorial/nativel.l/implementinR/svnc.html

[5] http://java.sun.com/applets/

[6] http://www.usenix.org/publications/java/usingjava3.html

[7] http://java.sun.com/produets/jdk/1.2/docs/api/java/lang/Thread.html

[8] http://java.sun.com/products/jdk/1.2/docs/api/java/lang/Runnable.html

[9] http://java.sun.com/products/jdk/1.2/docs/ajava/lang/Object.html



Última actualización: 28 de marzo de 2004
abia@dlsi.ua.es