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:
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 afalse
, 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
onotifyAll
- 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 valertrue
Un hilo despertado por
notify
tendrá que volver a luchar por conseguir el cerrojo.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
- Lo vas a encontrar prácticamente en cualquier lenguaje que admita concurrencia.
- Incluso implementaciones a nivel de biblioteca como GLib y con adaptaciones con un estilo más orientado a objetos como esta para el lenguaje Vala.
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 deTasks
a una cantidad menor de hilos.
- TaskPool
- Una cola de tareas es una cola FIFO de objetos de clase
Task
que se han enviado alTaskPool
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.
- Una cola de tareas es una cola FIFO de objetos de clase
9.7. Lenguaje D: Paralelismo
- Este lenguaje incorpora otras características asociadas a la
concurrencia, p.e.:
- Fibras
- Bucles
foreach
en paralelo - synchroniced al estilo de Java además de clases sincronizadas
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()
- Lock permite gestionar los cierres de forma explícita
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
10.5. Para saber más …
- Echa un vistazo al tutorial sobre concurrencia de Java.
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.