Anteriormente hemos visto:
Ahora veremos:
En el tutorial anterior te explique como funcionaba synchronized a nivel de código, en esta ocasión te voy a explicar el concepto que tiene, si tomamos el siguiente bloque de código:
//Estamos en un método al que acceden varios hilos a la vez, cerrojo //es una variable estática de la clase de tipo Object(como en el anterior tutorial).
synchronized(cerrojo){
x = x + 1;
}
Internamente lo que hace synchronized es dar el permiso de ejecutar ese bloque de código a un solo hilo esto sería dar el cerrojo al hilo, los demás hilos tienen que esperar a que este hilo termine de ejecutar esa sección, cuando este termina libera el cerrojo y otro hilo lo tomará, asi sucesivamente. Esta sería la lógica de la gestión del cerrojo, supongamos que el método cerrar() se encarga de que solo un hilo acceda hasta que este hilo llame al método abrir():
cerrojo.cerrar(); //El primer hilo que llegue obtiene el permiso. Los demás hilos tendrán que esperar a la ejecución de cerrojo.abrir().
hacer_operaciones();
cerrojo.abrir(); //El hilo libera el cerrojo adquirido, ahora otro hilo puede adquirirlo.
Gracias a esto no se tienen problemas de consistencia, pero tiene dos consecuencias:
Java tiene implementada una clase que modela los cerrojos, esta es la clase Lock y sus subclases además de diversas herramientas para la sincronización entre los hilos, los métodos que emplearemos para controlar el acceso seguro serán lock() y unlock(), solo basta con abarcar el código a proteger entre ellos y listo!, la clase Lock es una clase abstracta por lo que usaremos la subclase ReentrantLock y un ejecutor de capacidad variable:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Ejemplo extends Thread {
static int X_Compartida = 0;
static ReentrantLock cerrojo = new ReentrantLock();
public static void main(String[] args) {
int N = 10;
Runnable tareas[] = new Runnable[N];for (int i = 0; i < N; i++) {
tareas[i] = new Runnable() {
public void run() {
try {
cerrojo.lock();
X_Compartida = X_Compartida + 1;
} finally { //El unlock siempre tiene que ir en un bloque finally, en caso de error hay que liberar el cerrojo.
cerrojo.unlock();
}
}
};
}
ExecutorService ejecutor = Executors.newCachedThreadPool();for (int i = 0; i < N; i++) {
ejecutor.execute(tareas[i]);
}
ejecutor.shutdown();while (!ejecutor.isTerminated());System.out.println("El valor final de X es " + X_Compartida + " y deberia ser 10");
}
}
A parte del uso de cerrojos, Java ha implementado clases que modelan los tipos de datos básicos(int, float, …) con comportamiento atómico, es decir, las modificaciones se realizarán de forma segura aunque varios hilos estén modificando la variable de forma simultánea, para realizar esta modificaciones/consultas se utilizan los métodos que vienen definidos en estas clases:
import java.util.concurrent.atomic.AtomicInteger;
public class Ejemplo implements Runnable {
static AtomicInteger atomico = new AtomicInteger();
public void run() {
for (int i = 0; i < 10000; i++) {
atomico.incrementAndGet();
}
}
public static void main(String[] args) throws InterruptedException {
Thread hilos[] = new Thread[10];for (int i = 0; i < 10; i++) {// Cuando usamos la interfaz Runnable es necesario envolver las instancias de la// clase Ejemplo en instancias de Thread.
hilos[i] = new Thread(new Ejemplo());
}
for (int i = 0; i < 10; i++) {
hilos[i].start();
}
for (int i = 0; i < 10; i++) {
hilos[i].join();
}
System.out.println("El valor final de X es " + atomico.get() + " y deberia ser 100000");
}
}
Si, yo se que te estarás preguntando,¿por que diablos no has empezado con esto?, la respuesta es porque considero necesario que entiendas el origen del problema y las diferentes formas de tratarlo, ahora con las clases atómicas se nos abre un mundo de posibilidades.