Tema 5. Sincronización con Java

Contenidos

1. Exclusión mutua en JAVA

  • Java no tiene semáforos a nivel del lenguaje aunque sí en su biblioteca estándar.
  • Sin embargo proporciona otro tipo de primitivas con las que podemos gestionar los problemas de concurrencia
  • Utiliza un tipo particular de monitor (tema 6) incorporado en su sintaxis
  • En un enfoque orientado a objetos la concurrencia se traduce en que muchos hilos pueden estar ejecutando código de un mismo objeto
  • Conseguimos la exclusión mutua mediante la palabra reservada synchronized
  • Un método que lleve el modificador synchronized se ejecutará en exclusión mutua
  • Cuando un método sincronizado se está ejecutando no se ejecutará ningún otro método sincronizado del mismo objeto

2. Utilización de synchronized

  • Un ejemplo:

    1: public class Sincronizados {
    2:     public              void m1() {}
    3:     public synchronized void m2() {}
    4:     public              void m3() {}
    5:     public synchronized void m4() {}
    6:     public              void m5() {}
    7: }
    
  • Gráficamente:

    syncexample.png

    Figure 1: Ejemplo de sincronización en Java.

  • Un bloque de código también puede ser sincronizado. En este caso necesitamos hacer referencia a un objeto:

     1: public class Sincronizada {
     2:     public void metodo1() {
     3:           //instrucciones
     4: 
     5:           synchronized(this) {
     6:              //instrucciones que se ejecutarán en exclusión mutua con
     7:              //otro bloque synchronized
     8:           }
     9: 
    10:           //instrucciones
    11:      }
    12: }
    
  • El caso anterior lo podemos generalizar a cualquier objeto, no sólo this:

     1: public class Sincronizada {
     2:     public void metodo1() {
     3:           //instrucciones
     4: 
     5:           synchronized(otroObjeto) {
     6:              //instrucciones que se ejecutarán en exclusión mutua con
     7:              //el mutex de otroObjeto
     8:           }
     9: 
    10:           //instrucciones
    11:      }
    12: }
    
  • Podemos sincronizar a nivel de métodos de clase también:

     1: public class Sincronizada {
     2:     public synchronized static void metodo1() {
     3:           //instrucciones
     4:      }
     5: }
     6: // O tambien
     7: ...
     8: synchronized (nombreClase.class) {
     9:   //instrucciones
    10: }
    

3. Variables volatile

  • Las asignaciones entre variables de tipos primitivos se realizan de forma atómica, con lo cual no hace falta usar synchronized.
  • Las dos clases siguientes son equivalentes:

    public class s {               public class s1 {
     public void f() {              public void f() {
       synchronized (this) {            boolean b = true;
         boolean b = true;          }
       }                           }
     }
    }
    
  • Las optimizaciones del compilador pueden dar problemas:

    public class c {
       private boolean v = false;
       public void cambiaV() {
          v = true;
       }
       public synchronized void esperaVCierto() {
          v = false;
          if (v)
             //Código que no será ejecutado
       }
    }
    
  • Synchronized nos garantiza que ningún otro método sincronizado será ejecutado a la vez, pero el método cambiaV no es sincronizado, por lo tanto puede ser llamado mientras se está ejecutando esperaVCierto.
  • El compilador de Java entenderá que el código de dentro del if no se ejecutará nunca, ya que en el método esperaVCierto hemos inicializado v a false, por lo tanto eliminará este código.
  • Para evitar que el compilador haga estas optimizaciones declararemos v de tipo volatile
  • Declarar una variable de tipo volatile es decirle al compilador que otro hilo la puede modificar

4. Sincronización

wait() :
le indica al hilo en curso que abandone la exclusión mutua (libere el mutex) y se vaya al estado de espera hasta que otro hilo lo despierte
notify() :
un hilo, elegido al azar, del conjunto de espera pasa al estado de listo
notifyAll() :
todos los hilos del conjunto de espera pasan a listos

5. Sincronización, guarda boolena

synchronized void hacerCuandoCondicion() {
  while(!c)
     try {
       wait();
     } catch(InterruptedException e) {}

  // Aquí irá código que se ejecutará
  // cuando la condicion 'c' sea cierta
}

synchronized void hacerCondicionVerdadera() {
  c = true;
  notify();  // o notifyAll()
}
  • Vamos a tener dos conjuntos de hilos:
    • los que quieren acceder a la zona de exclusión mutua
    • los que están ``dormidos'' esperando a ser despertados por un notify o notifyAll
  • Cuando un hilo hace wait, primero se suspende el hilo y luego se libera el ``cerrojo'' (synchronized), con lo cual otro hilo puede entrar
  • El while es necesario, ya que que se haya despertado un hilo no nos garantiza que la variable c pase a valer true
  • Un hilo despertado por notify tendrá que volver a luchar por conseguir el cerrojo.

    boolguard.png

    Figure 2: Ejemplo de guarda booleana.

  • Puedes ver un ejemplo completo en este vídeo.

6. Ejemplos de código en Java

6.1. Productor y Consumidor: clase Buffer

 1: public class Buffer {
 2:   private int cima, capacidad, vector[];
 3: 
 4:   public Buffer(int i) {
 5:     cima = 0;
 6:     capacidad = i;
 7:     vector = new int[i];
 8:   }
 9: 
10:   synchronized public int extraer(int id) {
11:     System.out.println(id+": espera para leer");
12:     while (cima == 0)
13:       try {
14:         wait();
15:       } catch (InterruptedException e){}
16:     notifyAll();
17:     return vector[--cima];
18:   }
19: 
20:   synchronized public void insertar(int elem,int id) {
21:    System.out.println(id+": espera para insertar");
22:    while (cima == capacidad)
23:      try {
24:       wait();
25:      } catch (InterruptedException e){}
26:    vector[cima]=elem;
27:    cima++;
28:    notifyAll();
29:   }
30: }

6.2. Productor y Consumidor: clase Consumidor

 1: public class Consumidor extends Thread {
 2:   int elem, id;
 3:   Buffer buffer;
 4: 
 5:   Consumidor(Buffer b, int numHilo) {
 6:     buffer = b;
 7:     id = numHilo;
 8:     System.out.println("CONSUMIDOR " + id +": entra");
 9:   }
10: 
11:   public void run () {
12:     try {
13:       elem = buffer.extraer(id);
14:       System.out.println(id + ": he leido " + elem);
15:     } catch (Exception e) {}
16:   }
17: }

6.3. Productor y Consumidor: clase Productor

 1: public class Productor extends Thread {
 2:   Buffer buffer;
 3:   int elem, id;
 4: 
 5:   Productor(Buffer b, int i, int numHilo) {
 6:     elem = i;
 7:     buffer = b;
 8:     id = numHilo;
 9:     System.out.println("PRODUCTOR " + id + ": entra");
10:   }
11: 
12:   public void run () {
13:     try {
14:       buffer.insertar(elem, id);
15:       System.out.println(id + ": inserta " + elem);
16:     } catch (Exception e) {}
17:   }
18: }

6.4. Productor y Consumidor: clase Principal

 1: public class ProductorConsumidor {
 2:   static Buffer buf = new Buffer(3);
 3:   static int numcons = 7;
 4:   static int numprods = 5;
 5: 
 6:   public static void main(String[] args) {
 7:      for(int i = 1; i <= numprods; i++)
 8:        new Productor(buf,i,i).start();
 9: 
10:      for(int k = 1; k <= 1_000_000_000; k++);
11:        System.out.println();
12: 
13:      for(int j = 1; j <= numcons; j++)
14:        new Consumidor(buf, j).start();
15: 
16:      System.out.println("Fin del hilo main");
17:   }
18: }

6.5. Productor y Consumidor: traza

| PRODUCTOR 1: entra      | CONSUMIDOR 1: entra | CONSUMIDOR 5: entra |
| PRODUCTOR 2: entra      | CONSUMIDOR 2: entra | 4: espera para leer |
| 1: espera para insertar | 1: espera para leer | 4: he leido 2       |
| 1: inserta 1            | 4: inserta 4        | CONSUMIDOR 6: entra |
| PRODUCTOR 3: entra      | 1: he leido 3       | 5: espera para leer |
| 2: espera para insertar | CONSUMIDOR 3: entra | 5: he leido 1       |
| 2: inserta 2            | 2: espera para leer | 6: espera para leer |
| PRODUCTOR 4: entra      | 5: inserta 5        | CONSUMIDOR 7: entra |
| 3: espera para insertar | 2: he leido 4       | Fin del hilo main   |
| 3: inserta 3            | CONSUMIDOR 4: entra | 7: espera para leer |
| PRODUCTOR 5: entra      | 3: espera para leer |                     |
| 4: espera para insertar | 3: he leido 5       |                     |
| 5: espera para insertar |                     |                     |

7. Otros problemas clásicos

  • Vemos el código fuente de:
    • ReaderWriter.java
    • Philosopher.java

8. Semáforos

8.1. Semáforo binario

 1: public class SemaforoBinario {
 2:   protected int contador = 0;
 3: 
 4:   public SemaforoBinario(int valorInicial) {
 5:     contador = valorInicial; // 1 o 0
 6:   }
 7: 
 8:   synchronized public void WAIT () {
 9:     while (contador == 0 )
10:       try {
11:         wait();
12:       } catch (Exception e) {}
13:     contador--;
14:   }
15: 
16:   synchronized public void SIGNAL () {
17:     contador = 1;
18:     notify();
19:   }
20: }

8.2. Semáforo general

 1: public class SemaforoGeneral extends SemaforoBinario {
 2: 
 3:   public SemaforoGeneral(int valorInicial) {
 4:     super(valorInicial); // numero natural
 5:   }
 6: 
 7:   synchronized public void  SIGNAL () {
 8:     contador ++;
 9:     notify();
10:   }
11: }

9. ThreadPools

9.1. Conceptos básicos

  • En Java, los hilos se asignan/equiparan a hilos del sistema, que por tanto son recursos del Sistema Operativo.
  • Posibles problemas:
    • Si creamos hilos sin control, es posible que nos quedemos sin estos recursos rápidamente.
    • La creación de hilos es menos costosa que la de procesos, pero aun así impone una sobrecarga para el S.O.
  • Posibles problemas:
    • El S.O. es el encargado de realizar los cambios de contexto entre hilos.
    • Cuantos más hilos tengamos en marcha en un momento dado una parte importante del tiempo de ejecución de la aplicación se estará dedicando al cambio de contexto en lugar de a realizar el trabajo real para el que se ha programado.
  • El concepto de ThreadPool aparece para ahorrar recursos en una aplicación multi-hilo y para contener el paralelismo entre unos límites predefinidos.
  • Un ThreadPool nos permite controlar la cantidad de hilos que crea la aplicación durante su ciclo de vida.
  • También podemos planificar la ejecución de tareas y mantener las tareas entrantes en una cola hasta que puedan ser atendidas.
  • En lugar de crear y destruir un hilo para cada tarea, un ThreadPool mantiene un grupo fijo o dinámico de hilos que se reutilizan para ejecutar varias tareas.
  • Esto optimiza el uso de recursos del sistema y mejora el rendimiento de las aplicaciones concurrentes, sobre todo en situaciones de alta carga o con muchas tareas pequeñas o cortas.

9.2. Implementación en Java

  • La clase auxiliar Executors contiene varios métodos para la creación de instancias preconfiguradas de ThreadPools.

    Estas clases son válidas para empezar si no necesitamos emplear ajustes finos.

  • Usamos las interfaces Executor y ExecutorService para trabajar con diferentes implementaciones de ThreadPools en Java.

    Al usar interfaces mantenemos nuestro código desacoplado de la implementación real del ThreadPool.

9.3. Ejemplo en Java

 1: import java.util.concurrent.ExecutorService;
 2: import java.util.concurrent.Executors;
 3: 
 4: public class ThreadPoolExample {
 5:     public static void main(String[] args) {
 6:         // Crear un ThreadPool con 4 hilos
 7:         ExecutorService executor = Executors.newFixedThreadPool(4);
 8: 
 9:         // Enviar tareas al ThreadPool
10:         for (int i = 1; i <= 10; i++) {
11:             int taskNumber = i;
12:             executor.submit(() -> {
13:                 System.out.println("Ejecutando tarea " + taskNumber + " en " + Thread.currentThread().getName());
14:             });
15:         }
16: 
17:         // Apagar el pool
18:         executor.shutdown();
19:     }
20: }

9.4. ThreadPool no es exclusivo de Java

9.5. Ejemplo en Vala

 1: // valac --pkg glib-2.0 thread_pool.vala
 2: 
 3: class Worker {
 4:   public string thread_name { private set; get; }
 5:   public int x_times { private set; get; }
 6:   public int priority { private set; get; }
 7: 
 8:   public Worker (string name, int x, int priority) {
 9:     this.priority = priority;
10:     this.thread_name = name;
11:     this.x_times = x;
12:   }
13: 
14:   public void run () {
15:     for (int i = 0; i < this.x_times ; i++) {
16:       print ("%s: %d/%d\n", this.thread_name, i + 1, this.x_times);
17:       Thread.usleep (1000000); // wait a second
18:     }
19:   }
20: }
21: 
22: public static int main (string[] args) {
23:   try {
24:     ThreadPool<Worker> pool = new ThreadPool<Worker>.with_owned_data ((worker) => {
25:         // Call worker.run () on thread-start
26:         worker.run ();
27:       }, 3, false);
28: 
29:     // Define a priority (otpional)
30:     pool.set_sort_function ((worker1, worker2) => {
31:         // A simple priority-compare, qsort-style
32:         return (worker1.priority < worker2.priority)? -1 : (int) (worker1.priority > worker2.priority);
33:       });
34: 
35:     // Assign some tasks:
36:     pool.add (new Worker ("Thread 1", 5, 4));
37:     pool.add (new Worker ("Thread 2", 10, 3));
38:     pool.add (new Worker ("Thread 4", 5, 2));
39:     pool.add (new Worker ("Thread 5", 5, 1));
40: 
41:     uint waiting = pool.unprocessed ();     // unfinished workers = 4
42:     uint allowed = pool.get_max_threads (); // max running threads = 3
43:     uint running = pool.get_num_threads (); // running threads = 3
44:     print ("%u/%u threads are running, %u outstanding.\n", running, allowed, waiting);
45:   } catch (ThreadError e) {
46:     print ("ThreadError: %s\n", e.message);
47:   }
48: 
49:   return 0;
50: }

9.6. ThreadPool: lenguaje D

  • O bajo otros nombres, como es el caso del lenguaje D donde este concepto se conoce como TaskPool, creado en torno al concepto de Task.

    Esta clase (TaskPool) encapsula una cola de tareas y un conjunto de hilos trabajadores. Su propósito es asignar de manera eficiente una gran cantidad de Tasks a una cantidad menor de hilos.

  • TaskPool
    • Una cola de tareas es una cola FIFO de objetos de clase Task que se han enviado al TaskPool y están esperando su ejecución.
    • Un hilo de trabajo es un hilo que ejecuta la tarea (Task) al principio de la cola cuando hay una disponible y se queda inactivo cuando la cola está vacía.

9.7. Lenguaje D: Paralelismo

10. Más sobre concurrencia en Java

  • El API de Java 10:
    • La documentación general la puedes consultar en: java.util.concurrent
    • Allí encontrarás documentación sobre los cierres de exclusión mutua, barreras, semáforos, colecciones concurrentes (vectores, conjuntos, tablas, colas).
    • Veamos algunos de ellos.

10.1. Cerrojos (Locks) y Variables de Condición (Condition)

  • java.util.concurrent.locks
  • Interfaces Lock y Condition
    • Lock permite gestionar los cierres de forma explícita
      • lock()
      • unlock()
      • tryLock()
      • newCondition()
    • Condition permite establecer más de una cola asociada a un Lock (ejemplo BoundedBuffer)
      • await()
      • awaitUntil()
      • signal()
      • signalAll()

10.2. Colecciones seguras

BlockingQueue :
Define una estructura FIFO que se bloquea (o espera un tiempo máximo determinado) cuando se intenta añadir un elemento a una cola llena o retirar uno de una cola vacía. Dispone de diversas implementaciones, p.e. ArrayBlockingQueue.
ConcurrentMap :
Define una tabla hash con operaciones atómicas. Dispone de varias implementaciones, p.e. ConcurrentHashMap.

10.3. Tipos de datos atómicos

  • java.util.concurrent.atomic
  • Conjunto de clases que permiten realizar operaciones atómicas con variables de tipos básicos
    • AtomicBoolean
    • AtomicInteger
    • AtomicIntegerArray
    • AtomicLong
    • AtomicLongArray
  • Por ejemplo, AtomicInteger dispone de:
    • addAndGet
    • compareAndSet
    • decrementAndGet
    • getAndAdd
    • getAndDecrement
    • getAndIncrement
    • getAndSet
    • incrementAndGet
    • intValue
    • lazySet
    • set
  • Vemos el código de PhilosopherConditions.java

10.4. Executors

  • Representan el concepto de asincronicidad (async/await, promesas, futuros, etc…) en Java.
  • Echa un vistazo p.e. aquí y a este vídeo.
  • Pero también se emplean en la implementación de ThreadPools en Java.

10.5. Para saber más …

11. Aclaraciones

  • En ningún caso estas transparencias son la bibliografía de la asignatura, por lo tanto debes estudiar, aclarar y ampliar los conceptos que en ellas encuentres empleando los enlaces web y bibliografía recomendada que puedes consultar en la página web de la ficha de la asignatura y en la web propia de la asignatura.

Created: 2024-11-17 dom 18:37

Validate