CSI Consultores

 
  • Aumentar fuente
  • Fuente predeterminada
  • Disminuir fuente
Home Seccion de blogs admin Concurrencia en Java y problemas con DeadLocks
02.09.2009 08:59:00
admin

Por Guido J. Granobles.

Es conocido por muchos que  el uso de la palabra clave synchronized ya sea para el establecimiento de métodos o bloques de código sincronizado afecta el desempeño óptimo de las aplicaciones en cuanto a velocidad de procesamiento se refiere.  Aunque el uso abusivo de este tipo de bloques de código se ha condenado desde sus inicios cabe decir que las constantes mejoras de la JVM a través de los años hace que la comparación entre el porcentaje de desempeño afectado VS los grandes beneficios  a la hora de resolver problemas de concurrencia, inclinan la balanza hacia este ultimo.  El principal problema asociado con el uso excesivo de los bloques de código sincronizado está en que a medida que se incrementan este tipo de métodos o bloques en una aplicación, mayor es la posibilidad de que en algún punto del programa durante la ejecución del mismo se presenten los conocidos DeadLocks que hacen que la aplicación entre en un loop infinito obligando a que sea reiniciada. 

En concreto un DeadLock ocurre cuando como mínimo dos hilos intentan simultáneamente asegurar un objeto que el hilo contrario ya tiene. Para ser más claro expondré un ejemplo, el hilo A obtiene el Lock  del objeto1 (o asegura el objeto1) mientras el hilo B obtiene el Lock del objeto2, seguidamente el hilo A intenta obtener el Lock del objeto2 antes de liberar el Lock del objeto1, al mismo tiempo el hilo B intenta obtener el Lock del objeto1 antes de liberar el Lock del objeto2, lo que tenemos aquí es que el hilo  A  esperara hasta que el hilo B libere el Lock del objeto2  mientras el Hilo B está esperando a que el hilo A libere el Lock del objeto1 y así se quedaran esperando el uno al otro hasta el día del juicio final o hasta que alguien reinicie la aplicación o la maquina. Veamos el ejemplo en código:     


     public static Object obj1 = new Object();

     public static Object obj2 = new Object();
      public void metodo1() {
         synchronized (obj1) {
              synchronized (obj2) {
                    proceso1();
              }
            }
      }
     
      public void metodo2{
           synchronized (obj2) {
              synchronized (obj1) {
                     proceso2();
              }
        }    
     }

Podemos ver que tenemos dos bloques de código sincronizado en 2 métodos, primero  un hilo A invoca  a metodo1  y obtiene  primero el Lock de obj1, al mismo tiempo un Hilo B obtiene el Lock de obj2, seguidamente el Hilo intenta obtener el Lock de obj2 pero en vista de que el hilo B ya lo tiene debe esperar a que sea liberado, pero como se puede ver el hilo B estará esperando que A libere el Lock de obj1 y así habrá ocurrido un DeadLock. Ambos hilos se han quedado en una espera eterna. 

Aunque saber cuando ocurrirá un DeadLock es muy difícil, por cuanto se debe considerar que la posibilidad que dos hilos accesen los 2 métodos simultáneamente, es una posibilidad tal vez baja pero esta allí latente y puede ocurrir inesperadamente una vez en un periodo de tiempo o muchas veces dentro del mismo periodo, detectar este tipo de problemas analizando el código puede resultar agobiante, aunque un error como el del caso anterior puede detectarse con facilidad y concluir, que el problema a resolver en el código anterior es el orden en que se aseguran los objetos. En otras palabras deberían asegurarse en el mismo orden en ambos métodos y así se evitaría el posible DeadLock.  Pero no siempre es tan fácil determinar cuando existe la posibilidad de un DeadLock. Miremos un ejemplo más complejo: 

 public class Deadlock {
      static class Auto {
        private final String modelo;
        private final long kilometraje;
       
        public Auto(String modelo, long kl) {
            this.modelo = modelo;
            this.kilometraje = kl;
        }
        public String getModelo() {
            return this.modelo;
        }
        public synchronized void difKilometraje(Auto auto) {
            System.out.println("Calcular");
            System.out.println("Diferencia en Kilometraje " + Math.abs((auto.getKilometraje() - getKilometraje())));
                     
         }
        public synchronized long getKilometraje() {       
           System.out.println(" Get Kilometraje para " + getModelo());
           return kilometraje;
        }
       
 
    }

    public static void main(String[] args) {
        final Auto mazda = new Auto("Mazda", 60000);
        final Auto  renault = new Auto("Renault", 80000);
        new Thread(new Runnable() {
            public void run() { mazda.difKilometraje(renault); }
        }).start();
        new Thread(new Runnable() {
            public void run() { renault.difKilometraje(mazda); }
        }).start();
    }

Se puede ver que se crean dos instancias de la misma clase Auto, la cual tiene un método que se encargan de calcular la diferencia en kilometraje entre dos Autos, al ejecutar el primer hilo  cuando Mazda invoca al método difKilometraje obtiene el Lock del objeto Mazda, de otro lado en el segundo hilo sucede los mismo este segundo hilo obtiene el Lock del objeto Renault; pero que sucede cuando ambos hilos invocan el método getKilometraje  usando el objeto que se ha pasado como parámetro ?, pues sucede que el hilo uno intenta obtener el Lock de Renault mientras el hilo 2 intenta obtener el Lock de Mazda. Como puede notarse ninguno de los dos conseguirá lo que quiere y colgaran la aplicación.  Es exactamente como el primer caso que vimos, solo que esta vez no es tan obvio verlo a simple vista y en verdad las cosas podrán complicarse  aun más, cuando el código crece.   

Este caso es una semilla potencial para germinar en un DeadLock, si se ejecuta el código anterior en 10 ocasiones consecutivas probablemente se ejecutara correctamente 6 o 7 veces pero en el resto de ocasiones la aplicación se colgara. El hecho de que la aplicación se cuelgue unas veces y otras no, hace más difícil encontrar la causa del problema. Pero en el ejemplo anterior podría pensarse que no habría razón en la práctica real para que dos hilos diferentes se lanzaran a realizar tales operaciones, por lo tanto veamos un ejemplo típico un poco más cercano a la vida real.


Su pongamos que tenemos que realizar la transferencia de dinero entre 2 cajas para lo cual tendremos dos objetos caja1 y caja2 instancias de la misma clase caja, el código seria como esto:

  public class Deadlock {
    static class Caja {

        double saldo;
        public Caja(double salIni) {
            this.saldo = salIni;
        }
        public void debitar(double valor) {
            saldo += valor;
        }
        public void acreditar(double valor) {
            saldo -= valor;
        }
        public double getSaldo() {
            return saldo;
        }
    }
   
    static class OperaCaja{       
         public void transfer(Caja desde, Caja hacia, double valor) {
             System.out.println("transfiriendo saldo...");
             System.out.println("adquiriendo lock desde...");
             synchronized (desde) {
                 System.out.println("adquiriendo lock hacia...");
                 synchronized (hacia) {
                    if (desde.getSaldo() >= valor) {
                        desde.debitar(valor);
                        hacia.acreditar(valor);
                        System.out.println("transferencia terminada...");
                    }
                }
            }
         }
    }

    public static void main(String[] args) {
       
        final Caja caja1 = new Caja(60000);
        final Caja caja2 = new Caja(80000);
        final OperaCaja opc = new OperaCaja();       
        new Thread(new Runnable() {
            public void run() {
                opc.transfer(caja1, caja2, 20000);
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                opc.transfer(caja2, caja1, 10000);
            }
        }).start();
    }

Los mensajes impresos en cada paso no solo están allí para mostrarnos lo que está pasando durante la ejecución sino además para retrasar un poco la ejecución de los hilos como sucedería en la realidad en una aplicación que atiende múltiples llamados. Como era de esperarse este código está destinado a caer en un DeadLock en algún momento de    su ciclo de vida, esto debido a que diferentes hilos pueden llamar el mismo método con orden de parámetros diferentes tal como en los ejemplos anteriores.

Bien, ahora la pregunta que nos ha estado asaltando es como evitar que sucedan este tipo de problemas cuando necesitamos sincronizar el acceso a un objeto?, una de las posibles soluciones consiste en ordenar de manera muy cuidadosa el orden en que se llaman los métodos sincronizados desde diferentes hilos o el orden en que se pasan los parámetros a métodos que contienen bloques de código sincronizado observando en qué orden son adquiridos los locks en el método, esto suele ser una tarea muy dura cuando se trata de aplicaciones bastante complejas, además se requiere que se documente muy bien todo el código donde haga sincronización.  Afortunadamente las versiones más recientes de Java nos ofrecen un paquete que nos ayuda a lidiar con estos problemas, hablo del paquete java.util.concurrent.locks. 

Por ahora solo diré que este paquete nos ofrece una interface de nombre Lock la cual es implementada por la clase ReentrantLock aunque un objeto de este tipo funciona muy similar a los bloques de código sincronizado, al solo poder adquirir un Lock a la vez, su principal ventaja es el hecho de podernos permitir programar ejecuciones alternativas si el Lock del objeto ya ha sido adquirido por otro hilo. La forma de usar este objeto para resolver los problemas antes presentados lo dejo para otro post.

 

 

 

 


  demonios java | hilos | Locks | DeadLocks | concurrencia
 

Comentario
Inicio de sesión:

E-mail:




Encuentranos en FaceBook

Blog

« <September - 2010> »
SunMonTueWedThu FriSat
   1234
567891011
12131415161718
19202122232425
2627282930  
Today

Enlazes de interes