Un problema básico del multithreading es cuando varios programas (o, para el caso, varios threads) acceden a los mismos datos: ¿cómo sabemos si uno de ellos no los modifica mientras los está usando otro?.
Veamos un ejemplo, donde suponemos que varios threads usan la variable valorImportante:
if (valorImportante > 0 ) {
..... algo se procesa acá ........
valorImportante = valorImportante - 1;
..... sigue.....................
}
¿Cómo nos aseguramos que valorImportante no cambió entre el if y la línea resaltada? Otros threads pueden haberlo modificado mientras tanto. Asimismo, puede suceder que dos threads estén ejecutando la misma porción de código, y se pierda uno de los decrementos. Imaginen algo así:
(antes) valorImportante = 10
(thread 1) lee valorImportante = 10
(thread 2) lee valorImportante = 10
(thread 1) 10 -1 = 9
(thread 2) 10 -1 = 9
(thread 2) asigna 9 a valorImportante
(thread 1) asigna 9 a valorImportante
(después) valorImportante = 9
Como vemos, a pesar de haber restado dos veces, hemos perdido una de las restas. Aunque usemos -= en vez de la resta es lo mismo, porque el código igualmente se resuelve en varios pasos (varias
operaciones atómicas).
Para evitar esto, Java nos brinda la palabra clave Synchronized, que bloquea el acceso a una variable a todos los threads menos el que lo está usando.
Vamos a ver un caso específico; se trata de dos contadores que usan el mismo sumador para sumar de a uno una cantidad a. Supuestamente entre los dos deben llevar el sumador (a) hasta 20000.
Archivo Ejemplo22.java, compilar con javac Ejemplo22.java, ejecutar con java Ejemplo22
public class Ejemplo22 {
public static void main(String argv[]) {
Sumador A = new Sumador(); un único sumador
Contador C1 = new Contador(A); dos threads que lo usan...
Contador C2 = new Contador(A); ...para sumar
}
}
class Contador extends Thread {
Sumador s;
Contador (Sumador sumador) {
s = sumador; le asigno un sumador a usar
}
public void run() {
s.sumar(); ejecuto la suma
}
}
class Sumador {
int a = 0;
public void sumar() {
for (int i=0; i<10000; i++ ) {
if ( (i % 5000)
0 ) { "%" da el resto de la división:
System.out.println(a); imprimo cada 5000
System.out.println(a); imprimo el final
}
}
Ejecutando esto nos da más o menos así (cada corrida es diferente, dependiendo de cómo se "chocan" los threads y la carga de la CPU):
C:\java\curso>java Ejemplo22
0
87
8926
10434
14159
17855
Esto se debe justamente a lo que explicábamos al principio: a veces los dos threads intentan ejecutar a += 1 simultáneamente, con lo que algunos incrementos se pierden.
Podemos solucionar esto modificando el método run():
public void run() {
synchronized (s) {
s.sumar();
}
}
Con esto, sólo a uno de los dos threads se les permite ejecutar s.sumar() por vez, y se evita el problema. Por supuesto, el otro thread queda esperando, por lo que más vale no utilizar esto con métodos muy largos ya que el programa se puede poner lento o aún bloquearse.
La salida ahora será:
C:\java\curso>java Ejemplo22
0 <
5000 < primer thread
10000 <
10000 (
15000 ( segundo thread
20000 (
Lo mismo logramos (y en forma más correcta) declarando como synchronized al método sumar():
public synchronized void sumar() { .............
Esto es mejor porque la clase que llama a sumar() no necesita saber que tiene que sincronizar el objeto antes de llamar al método, y si otros objetos (en otros threads) lo llaman, no necesitamos preocuparnos.