Programación Concurrente

Hilos (Threads)

Los hilos en Java te permiten ejecutar múltiples tareas de forma concurrente, lo que es útil para aprovechar al máximo los recursos del sistema y mejorar el rendimiento de las aplicaciones.

Creación de Hilos

Puedes crear hilos en Java extendiendo la clase Thread o implementando la interfaz Runnable.

Extender la clase Thread
				
					public class MiHilo extends Thread {
    public void run() {
        // Código a ejecutar en el hilo
        for (int i = 0; i < 5; i++) {
            System.out.println("Hola desde el hilo " + i);
            try {
                Thread.sleep(1000); // Esperar 1 segundo
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        MiHilo hilo = new MiHilo();
        hilo.start(); // Iniciar el hilo
    }
}

				
			
Implementar la interfaz Runnable
 
				
					public class MiRunnable implements Runnable {
    public void run() {
        // Código a ejecutar en el hilo
        for (int i = 0; i < 5; i++) {
            System.out.println("Hola desde el hilo " + i);
            try {
                Thread.sleep(1000); // Esperar 1 segundo
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        MiRunnable miRunnable = new MiRunnable();
        Thread hilo = new Thread(miRunnable);
        hilo.start(); // Iniciar el hilo
    }
}

				
			

En este ejemplo:

  • Creamos una clase MiRunnable que implementa la interfaz Runnable y define el método run() que contiene el código que se ejecutará en el hilo.
  • Creamos un objeto Thread pasando el objeto MiRunnable en su constructor.
  • Iniciamos el hilo llamando al método start().

Ventajas de Usar Runnable

  • Flexibilidad: Permite que una clase implemente otras interfaces o herede de otras clases, lo que no sería posible si extendiera la clase Thread.
  • Reutilización de Código: Puedes reutilizar la misma instancia de Runnable para iniciar múltiples hilos.

Sincronización

La sincronización en Java es importante cuando múltiples hilos acceden y modifican recursos compartidos para evitar condiciones de carrera y garantizar la consistencia de los datos.

Uso de la palabra clave synchronized

Puedes utilizar la palabra clave synchronized para sincronizar bloques de código o métodos.

Sincronización de Bloques de Código
				
					public class Contador {
    private int count = 0;

    public void increment() {
        synchronized(this) {
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}

				
			

En este ejemplo, el bloque de código dentro del método increment() está sincronizado utilizando synchronized(this), lo que garantiza que solo un hilo pueda ejecutar ese bloque a la vez.

Sincronización de Métodos
				
					public class Contador {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

				
			

En este caso, los métodos increment() y getCount() están sincronizados, lo que significa que solo un hilo puede ejecutar cualquiera de estos métodos a la vez.

Bloques synchronized estáticos

También puedes sincronizar bloques de código estáticos utilizando un objeto de clase.

				
					public class Contador {
    private static int count = 0;

    public static void increment() {
        synchronized(Contador.class) {
            count++;
        }
    }

    public static int getCount() {
        return count;
    }
}

				
			

En este ejemplo, el bloque de código estático está sincronizado utilizando synchronized(Contador.class).

Problemas comunes de concurrencia (deadlock, race conditions)

Al trabajar con hilos en Java, es importante tener en cuenta algunos problemas comunes que pueden surgir debido a la concurrencia, como el deadlock y las race conditions.

Deadlock

El deadlock ocurre cuando dos o más hilos se bloquean mutuamente esperando recursos que se están utilizando por otros hilos, lo que hace que todos los hilos queden bloqueados y la aplicación se quede inactiva.

				
					public class DeadlockEjemplo {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        Thread hilo1 = new Thread(() -> {
            synchronized(lock1) {
                System.out.println("Hilo 1: Obtuvo el lock1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized(lock2) {
                    System.out.println("Hilo 1: Obtuvo el lock2");
                }
            }
        });

        Thread hilo2 = new Thread(() -> {
            synchronized(lock2) {
                System.out.println("Hilo 2: Obtuvo el lock2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized(lock1) {
                    System.out.println("Hilo 2: Obtuvo el lock1");
                }
            }
        });

        hilo1.start();
        hilo2.start();
    }
}

				
			

En este ejemplo, hilo1 bloquea lock1 y espera lock2, mientras que hilo2 bloquea lock2 y espera lock1. Ambos hilos quedan bloqueados esperando los recursos que están siendo utilizados por el otro hilo, lo que resulta en un deadlock.

Race Conditions

Las race conditions ocurren cuando múltiples hilos intentan modificar el mismo recurso compartido al mismo tiempo sin una sincronización adecuada, lo que puede llevar a resultados inesperados o inconsistentes.

				
					public class RaceConditionEjemplo {
    private static int count = 0;

    public static void main(String[] args) {
        Thread hilo1 = new Thread(() -> {
            for (int i = 0; i < 1000000; i++) {
                count++;
            }
        });

        Thread hilo2 = new Thread(() -> {
            for (int i = 0; i < 1000000; i++) {
                count++;
            }
        });

        hilo1.start();
        hilo2.start();

        // Esperar a que los hilos terminen
        try {
            hilo1.join();
            hilo2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Valor de count: " + count);
    }
}

				
			

En este ejemplo, ambos hilos incrementan la variable count en un millón de veces, pero debido a la falta de sincronización, pueden interferirse mutuamente y el resultado final podría no ser dos millones como se espera.