java - ejercicios - ¿Hay una condición de carrera en este ejemplo? Si es así, ¿cómo podría evitarse?
mutex (3)
Estoy viendo algunos ejemplos de notificación / espera y me encontré con este. Entiendo que un bloque sincronizado esencialmente define una sección crítica, ¿pero esto no presenta una condición de carrera? Nada especifica qué bloque sincronizado se ingresa primero.
public class ThreadA {
public static void main(String[] args){
ThreadB b = new ThreadB();
b.start();
synchronized(b){
try{
System.out.println("Waiting for b to complete...");
b.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("Total is: " + b.total);
}
}
}
class ThreadB extends Thread {
int total;
@Override
public void run(){
synchronized(this){
for(int i=0; i<100 ; i++){
total += i;
}
notify();
}
}
}
Salida por sitio web:
Esperando que b se complete ...
El total es: 4950
Sí, es una carrera en cuanto a qué hilo entra qué bloque sincronizado primero. Para la mayoría de los escenarios de la carrera, la salida y la respuesta serán las mismas. Para uno, sin embargo, el programa se estancará:
- Principal inicia llamadas b.start () e inmediatamente se programa.
- El hilo B comienza, ingresa sincronizado, llama a notify ().
- Main ingresa su bloque sincronizado, llamadas wait ()
En este caso, main esperará por siempre ya que el subproceso b llamado notify before main está bloqueado en wait ().
Dicho esto, es poco probable, pero con todos los hilos, debe concluir que sucederá y luego en el peor momento posible.
Sí, es una condición de carrera. Nada impide que ThreadB se inicie, ingrese su método de ejecución y se sincronice consigo mismo antes de que ThreadA ingrese a su bloque sincronizado (por lo tanto, espera indefinidamente). Sin embargo, es muy poco probable que suceda, teniendo en cuenta el tiempo que tarda un nuevo hilo en comenzar la ejecución.
La manera más fácil y más recomendada para manejar este tipo de situaciones es no escribir su propia implementación, sino optar por usar un futuro / invocable provisto por un Ejecutor.
Para arreglar este caso particular sin seguir los estándares:
- Establezca un valor booleano ''terminado'' establecido al final del bloque sincronizado de ThreadB.
- Si el booleano ''finished'' es verdadero después de ingresar el bloque sincronizado, entonces no debe llamar a wait.
Correcto, no está garantizado qué hilo se ejecutará primero. El hilo b podría hacer su notificación antes de que el hilo principal comience a esperar.
Además de eso, un hilo puede regresar de esperar sin haber sido notificado, por lo que establecer un indicador y verificarlo antes de ingresar a la espera técnicamente no es suficiente. Podrías reescribirlo a algo como
public class ThreadA {
public static void main(String[] args) throws InterruptedException {
ThreadB b = new ThreadB();
b.start();
synchronized(b){
while (!b.isDone()) {
System.out.println("Waiting for b to complete...");
b.wait();
}
System.out.println("Total is: " + b.total);
}
}
}
class ThreadB extends Thread {
int total;
private boolean done = false;
@Override
public void run(){
synchronized(this){
for(int i=0; i<100 ; i++){
total += i;
}
done = true;
notify();
}
}
public boolean isDone() {return done;}
}
de modo que el hilo principal esperará hasta que b termine con su cálculo, independientemente de quién comience primero.
Por cierto, la documentación API recomienda que no se sincronice en los hilos. JDK se sincroniza en hilos para implementar Thread # join. Un hilo que termina envía un notifyAll que recibe todo lo que se une a él. Si tuviera que llamar a notify o notifyAll desde un hilo que haya adquirido el candado, algo que se uniera podría regresar temprano. Un efecto secundario de esto aquí es que si elimina la notificación, el código funciona de la misma manera.