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 poner synchronized.
  • Las dos clases siguientes son iguales:

    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.

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. Otras utilidades para el uso de concurrencia en el API de 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.

9.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()

9.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.

9.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

9.4. Para saber más …

10. 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: 2023-12-15 vie 13:35

Validate