Logo
Programación en Java SE 6 desde cero: Parte 4

Programación en Java SE 6 desde cero: Parte 4

Programación en Java SE 6 desde cero: Parte 4 

CONCURRENCIA

 

  1. Threads y Runnables

  2. Estados

  3. Bloqueos y Seguridad

  4. Uso de los métodos

 

Objetivos:

 

Conocer como se ejecutan los programas Java, así como crear hilos (threads) de ejecución independientes.

Analizar el comportamiento de la ejecución de los programas y los estados en los que puede encontrarse los hilos de ejecución.

Estudiar la seguridad y protección de los hilos de ejecución ante bloqueos (deadlocks).

 

Threads y Runnables en JAVA

 

Los hilos (Threads) en Java es la forma de realizar programación concurrente, ya que permiten lanzar dentro de un proceso tareas para que se ejecuten al mismo tiempo.

 

Generar hilos en JAVA

 

Para generar hilos en Java debemos hacerlo a nivel de programación, indicando de esta forma en nuestro código al sistema operativo que tareas o porciones de código Java se pueden ejecutar de forma concurrente. Esto el lenguaje lo hace posible a través de la clase Thread y la interfaz Runnable. Un hilo tiene dos componentes:

  • El objeto thread como tal, que comienza y tiene una prioridad y un estado, y es programado para ejecutarse por la máquina virtual Java.

  • El objetivo ejecutable, que contiene el código que se ejecuta cuando finalmente el hilo se le da a la CPU.

 

La clase Thread y la interface Runnable

 

La clase Thread representa un objeto hilo en Java, y la interface Runnable se utiliza para definir un objetivo ejecutable, aunque para nosotros como programadores es lo mismo, y conseguiremos el mismo objetivo. El hecho de tener una interface, si recordamos, es porque en Java no se permite la herencia múltiple y si una clase en nuestro diseño ya extiende de otra y necesitamos hacer de ella una tarea deberemos hacerlo con la interface. Por lo tanto en Java podemos programar un hilo de dos formas:

  • Escribir una clase que implemente la interface Runnable y pasar una instancia al constructor de una nueva clase Thread, para conseguir ejecutarlo, ya que cuando el Thread se arranca y llega a la CPU el método run de nuestro objeto es invocado.

  • Escribir una clase que herede de Thread y sobrescribir el método run. Cuando el hilo se arranca y llega a la CPU, el método run que hemos sobrescrito es ejecutado.

 

En cualquier caso a nivel de programación el código que queremos que se ejecute de forma paralela debemos implementarlo en un método run que tiene la siguiente sintaxis:

public void run();

 

Implementando la interface Runnable

 

Este método representa el código que se ejecuta cuando toca turno de CPU. Debemos tener en cuenta que siempre que queremos ejecutar un hilo, no sólo se debe crear una instancia, sino que además se debe arrancar con el método start.

A continuación veremos un ejemplo de implementación de la misma lógica primero implementando la interface Runnable y después extendiendo de la clase Thread.

public class PruebaRunnable implements Runnable {

  private String mensaje;

  public PruebaRunnable(String mensaje) {}

  this.mensaje = mensaje;

}

public void run(){

  for(int i= 1; i<=10;i++) {

    System.out.println(mensaje);

  }

System.out.println("Final del método run");

  }

}

 

Primero escribimos el código que vamos a ejecutar en una clase que implemente la interface, lo que nos obliga a rescribir el método run.

A continuación hemos generado un principal para simular la ejecución de un programa cualquiera que necesite lanzar esta tarea en paralelo a su propio programa.

public class PrincipalRunnable {

  public static void main(String [] args) {

    PruebaRunnable hello = new PruebaRunnable

    ("Saludo desde el hilo");

 

    Thread t = new Thread(hello);

    t.start();

    for(int x=1; x<=10;x++){

      System.out.println("Final del método main");

  }

}

 

De la clase principal debemos fijarnos como se crea una instancia de Thread y se le pasa una instancia de la clase que implementa Runnable, y luego como se invoca al método start. La salida en cada ordenador que lo ejecutemos será diferente, incluso en un mismo ordenador de una ejecución a otra variará en función de la asignación de CPU y memoria a la maquina virtual Java. Esta es una salida de nuestra ejecución donde se puede observar como dentro de nuestro programa se han ejecutado dos hilos en paralelo, el principal y el que se ha lanzado desde el principal que imprime Saludo desde el hilo.

Si nos fijamos ha finalizado antes el programa principal y el segundo hilo ha seguido ejecutándose.

Iteracion desde el principal 1

Saludo desde el hilo

Iteracion desde el principal 2

Iteracion desde el principal 3

Saludo desde el hilo

Iteracion desde el principal 4

Iteracion desde el principal 5

Iteracion desde el principal 6

Iteracion desde el principal 7

Iteracion desde el principal 8

Iteracion desde el principal 9

Iteracion desde el principal 10

Saludo desde el hilo

Final del metodo main

Saludo desde el hilo

Saludo desde el hilo

Saludo desde el hilo

Saludo desde el hilo

Saludo desde el hilo

Saludo desde el hilo

Saludo desde el hilo

Final del metodo main

 

Extendiendo la clase Thread:

 

Ahora vamos a ver lo mismo pero heredando de Thread:

 

public class PruebaThread extends Thread {

  private String mensaje;

  public PruebaThread(String mensaje) {

    this.mensaje = mensaje;

}

public void run() {

for (int i=1; i<=10;i++) {

System.out.println(mensaje);

  }

  System.out.println("Final del método run");

  }

}

 

Vemos que es prácticamente igual en este caso, únicamente hemos variado la forma que tenemos de generar herencia. Ahora programamos la clase principal de la ejecución donde también varían pequeños matices:

 

public class PrincipalThread {

  public static void main(String []args) {

    PruebaThread t = new PruebaThread

    ("Saludo desde el hilo");

    t.start();

    for(int x=1;x<=10;x++) {

      System.out.println("Iteracion desde el principal " + x);

  }

  System.out.println("Final del método main");

  }

}

Salida de la ejecución. Si vemos la salida también varía sobre la anterior, y seguramente sobre cualquier ejecución que se haga de este mismo código:

Saludo desde el hilo

Iteracion desde el principal 1

Saludo desde el hilo

Iteracion desde el principal 2

Saludo desde el hilo

Saludo desde el hilo

Saludo desde el hilo

Saludo desde el hilo

Saludo desde el hilo

Saludo desde el hilo

Iteracion desde el principal 3

Saludo desde el hilo

Iteracion desde el principal 4

Saludo desde el hilo

Iteracion desde el principal 5

Saludo desde el hilo

Iteracion desde el principal 6

Iteracion desde el principal 7

Iteracion desde el principal 8

Iteracion desde el principal 9

Iteracion desde el principal 10

Final del método main

 

Estados en JAVA

 

Los hilos desde que arrancan hasta que se completa la ejecución de todas las sentencias del método run pasan por varios estados. Debemos conocer y ser capaces de reconocer todos estos estados. Antes de nada debemos ser conscientes de los métodos que tienen todos los hilos para obtener el estado de un hilo:

 

public Thread.State getState()

 

Thread.State es una enumeración que está definida dentro de la clase Thread y que puede tener los siguientes estados:

 

NEW: El hilo se ha instanciado pero no se ha arrancado (start).

RUNNABLE: El hilo se está ejecutando en la CPU o esperando a ser planificado por la JVM  para ejecución.

WAITING: El hilo esta esperando a otro hilo para llevar a cabo una determinada acción.

TIMED_WAITING: Este estado es similar a WAITING solo que el hilo esta esperando por un tiempo determinado, no esta esperando a una notificación como es el caso anterior.

TERMINATED: El hilo ha finalizado la ejecución de todo su contenido. Un hilo en este estado no puede volver a iniciarse.

 

New, es un estado donde es método start del hilo no ha sido invocado, pero sí que hemos instanciado el hilo.

En el momento que invocamos al método start de la clase Thread el estado del hilo pasa a estado RUNNABLE.

Los hilos que están en estado RUNNABLE pueden estar ejecutandose o en espera a ser planificados por la JVM, que planifica los hilos que están en este estado en función de unas prioridades. La prioridad no es mas que un valor entero que marca que hilos ejecutan tareas más importantes y por lo tanto deben planificarse antes que otros. 

La prioridad es un valor que heredamos del hilo principal que nos lanza, aunque se puede modificar en cualquier momento utilizando el método de la clase Thread:

 

public final void setPriority(int p)

Típicamente se modifica la prioridad de un hilo con unas constantes que están predefinidas en la clase Thread para este uso y son: MIN_PRIORITY, NORM_PRIORITY y MAX_PRIORITY. El método para fijar la prioridad puede lanzar una excepción IllegalArgumentException si no establecemos una prioridad que su valor se encuentre entre MIN_PRIORITY y MAX_PRIORITY.

 

Para transitar al estado TIME_WAITING la clase Thread cuenta con el método sleep con el que conseguimos que el hilo sufra un retardo del tiempo que indicamos mediante un parámetro.

El método yield consigue liberar el uso de CPU por si algún hilo esta esperando por CPU, permitiendo planificarlo, aunque a diferencia del método anterior el estado del hilo permanece en estado RUNNABLE. 

También disponemos en la clase Thread de los métodos wait. Que dejan el hilo esperando hasta que recibe una notificación desde otro hilo que puede continuar, que veremos con los métodos notify y notifyAll.

El método wait tiene tres versiones sobrecargadas:

  • public final void wait() throws InterruptedException

  • public final void wait(long timeout) throws InterruptedException

  • public final void wait(long timeout, int nanos) throws InterruptedException

Estos métodos hacen que hilo quede en espera hasta que notify o nofityAll son invocados en el mismo objeto o se has esperado por el tiempo deseado, que corresponde a los métodos sobrecargados donde se puede indicar el tiempo por el que queremos que nuestro hilo espere.

 

Los métodos wait y notify no pueden ser invocados a no ser que el hilo tenga el objeto bloqueado. Cuando invocamos wait el bloqueo sobre el objeto lo liberamos para que otro hilo pueda acceder a este objeto y el estado del hilo se cambia a WAITING o TIMED_WAITING en función del método wait al que hayamos invocado. Cuando invocamos a notify en el objeto, los threads que estan en espera pasan a estado BLOCKED ya que el objeto en el que están esperando realmente no está disponible (otro hilo ha invocado a notify y se encuentra en el objeto).

 

Un hilo que ha llegado al estado TERMINATED es que ha finalizado la ejecución del método run. Cuando un hilo termina su ejecución no puede volver a comenzar, y se renocen como hilos muertos.

La única transición que se puede realizar a TERMINATED es desde RUNNABLE, y si intentamos arrancar el hilo de nuevo con el método start se lanzará una excepción IllegalThreadException.

 

Bloqueos y Seguridad

 

Sincronización de los hilos

 

La sincronización de hilos implica el bloqueo de objetos para proteger su estado (sus atributos). En Java, cuando programamos, necesitamos sincronizar hilos porque comparten la misma memoria, y es posible que dos hilos dejen datos inconsistentes en una aplicación al leer o modificar datos a la vez.

 

Para ver el por qué de la necesidad de la sincronización, vamos a ver un ejemplo donde dos hilos trabajan con un objeto en conjunto y como interfiere uno con el otro. Primero tenemos la implementación de una pila que puede guardar el contenido de diez elementos de tipo entero. El campo index apunta el siguiente espacio disponible en la pila. En la pila después de insertar el elemento en ella y antes de aumentar el contador hemos puesto una llamada al método Tread.yield() que normalmente no introduciríamos este código pero así somos capaces de simular que existe en ese punto una lógica de negocio importante que la clase está ejecutando y existe una demora por proceso.

 

Ejemplos de programa que hacen uso de  hilos en JAVA

 

public class PruebaPila {

      private int [ ] valores = new int[10];

      private int index = 0;

      public void push(int x) {

            if(index <= 9) {

                   valores[index] = x;

                   Thread.yield();

                   index++;

            }

      }

      public int pop() {

            if(index > 0) {

                   index--;

                   return valores[index];

            } else {

                   return -1;

            }

      }

      public String toString() {

            String contenido = "";

            for(int i = 0; i < valores.length; i++) {

                   contenido += valores[i] + " ";

            }

 

            return contenido;

      }

}

 

Ahora imaginemos que dos hilos ponen al mismo tiempo elementos en la pila, es posible que los datos de la misma estuvieran corruptos. Para demostrar eso vamos a programar un hilo que introduzca elementos en la pila.

 

public class Pusher extends Thread {

      private PruebaPila miPila;

 

      public Pusher(PruebaPila pila) {

           this.miPila = pila;

      }

      public void run() {

           for(int i = 1; i <= 5; i++) {

                miPila.push(i);

           }

      }

}

 

Por último vamos a generar un programa principal, que instancie dos hilos y compartan una misma pila e introduzcan elementos al mismo tiempo en la misma.

 

public class PrincipalPila {

      public static void main(String args[ ]) {

            PruebaPila pilaCompartida = new PruebaPila();

            Pusher one = new Pusher(pilaCompartida);

            Pusher two = new Pusher(pilaCompartida);

            one.start();

            two.start();

            try {

                 one.join();

                 two.join();

                }catch(InterruptedException ex) {

                    ex.printStackTrace();

            }

            System.out.println(pilaCompartida.toString());

      }

}

El resultado de esta ejecución es el siguiente:

1 1 3 2 4 3 5 4 0 5

 

Que aunque en cada ejecución, según las características de nuestros sistema operativo, conseguiremos un resultado diferente, podemos ver que los elementos que están en la pila no son coherentes, ya que hemos perdido elementos, hemos metido diez y sólo hay nueva ya que existe un cero.

El método join que se invoca desde la clase PrincipalPila hace que una vez finalizado el Thread espere a la finalización del resto de hilos para continuar con el programa principal, por lo tanto, cuando imprimimos el resultado de la pila sabemos que los dos hilos han finalizado su ejecución, sino podría ocurrir que imprimiéramos la pila y alguno de los dos hilos no hubiera finalizado aún.

 

Bloqueo en JAVA

 

Para todos los objetos en Java existe una entidad llamada monitor lock, que utilizan los diferentes hilos para sincronizar el acceso a los datos, y no hacerlo de forma concurrente que como hemos podido ver puede causar que se corrompan. Este monitor de bloqueo tiene las siguientes características:

  • Un hilo utiliza la palabra reservada syncronized para bloquear el acceso a un objeto, por lo que no permite que otros hilos accedan hasta que no lo libere.
  • Una vez que el hilo deja el bloque de código sincronizado, el bloqueo se libera.
  • Si un hilo intenta bloquear un objeto y ya está bloqueado, el hilo pasa a estado BLOCKED. El hilo permanece en este estado hasta que el bloqueo del objeto que se va a utilizar se libera.

 

Simulación de Bloqueo en JAVA

 

Un hilo intenta bloquear un objeto cuando:

  • Accede a un segmento de código sincronizado.

  • Accede a un método sincronizado.

 

Cuando sincronizamos un bloque de código o un método conseguimos establecer bloqueos en esa porción de código contenida por el ámbito que se genera por las llaves y generamos un espacio que en Java se denomina thread-safe. Recurriendo al ejemplo anterior de la pila vemos que si sincronizamos todos sus métodos no perdemos ningún elemento. El código resultante es:

public class PruebaPila {

 

      private int [ ] valores = new int[10];

      private int index = 0;

      public synchronized void push(int x) {

           if(index <= 9) {

               valores[index] = x;

               Thread.yield();

               index++;

           }

      }

      public synchronized int pop() {

           if(index > 0) {

               index--;

               return valores[index];

           } else {

               return -1;

           }

      }

      public synchronized String toString() {

           String contenido = "";

           for(int i = 0; i < valores.length; i++) {

               contenido += valores[i] + " ";

           }

 

           return contenido;

      }

}

Y esta es la salida que ha generado, aunque puede que al ejecutarlo tu salida sea diferente ya que no se establece ninguna política de establecimiento de elementos, pero si vemos que están todos:

1 1 2 2 3 3 4 4 5 5

 

Por lo tanto los datos contenidos en la pila son consistentes.

 

Ahora vamos a hablar de los métodos wait, notify y motifyAll que están definidos en la clase Object y por lo tanto son heredadas por todas las clases que existen en Java. El método wait hace que el hilo que actualmente está ejecutando el código para de ejecutarse hasta que otro hilo invoque al método notify o nitifyAll en el mismo objeto que avisarán al que está esperando. Debemos tener en cuenta lo siguiente sobre estos métodos:

  • Un hilo solo puede invocar a wait, notify y notifyAll en un objeto si un hilo esta bloqueandolo. Por lo tanto para invocar estos métodos debemos estar dentro de un ámbito de código sincronizado.
  • El método wait libera el bloqueo y transita a estado WAITING o TIMED_WAITING.

 

Seguimos con el curso de Java en el siguiente enlace:

Programación en Java SE 6 desde cero: Parte 5/7

Las dudas que os puedan comentarlas justo aquí debajo e iremos respondiendo. 

¡Deja un comentario!

Página Web diseñada por PJrN. Copyright © Votatuprofesor.com 2013-2024.