Soluciones simples para problemas de subprocesos de Java

Encora | 19 de junio, 2019

 

¡Empecemos sin demora! Hilos, ¿qué es un hilo?

Un hilo es la ruta que se sigue al ejecutar un programa. Todos los programas Java tienen al menos un hilo, conocido como hilo principal, que es creado por la Máquina Virtual Java. Un hilo es un único flujo de control secuencial dentro de un programa.

Un solo hilo también tiene un comienzo, una secuencia y un final. En cualquier momento del tiempo de ejecución del subproceso, existe un único punto de ejecución. Sin embargo, un hilo en sí mismo no es un programa y no se puede ejecutar por sí solo. Más bien, se ejecuta dentro de un programa y aprovecha los recursos asignados para ese programa y su entorno.

La verdadera emoción que envuelve los hilos no se trata de un solo hilo secuencial. Implica el uso de varios subprocesos que se ejecutan al mismo tiempo para realizar diferentes tareas, como gestionar grandes cantidades de datos u operaciones que requieren largos tiempos de ejecución que pueden ralentizar la aplicación e impedir una buena experiencia de usuario.

 

Algunas Ventajas

  1. Reducir el tiempo de desarrollo
  2. Mejora el desempeño de operaciones complejas
  3. Tareas paralelas

Desventajas

  1. Varios subprocesos pueden interferir entre sí al compartir recursos de hardware
  2. Pueden causar bloqueos, inanición y problemas de carrera si no se utilizan.

 

Problema de Interbloqueo

Un problema de interbloqueo describe una situación en la que dos o más subprocesos se bloquean para siempre, esperando el uno al otro. El interbloqueo ocurre cuando varios subprocesos necesitan el mismo objeto pero lo obtienen en un orden diferente. ¡Veamos el código!


public class DeadLockThread {

public static Object object1 = new Object();
public static Object object2 = new Object();

public static void main(String args[]) {
Thread1 thread1 = new Thread1();
Thread2 thread2 = new Thread2();
thread1.start();
thread2.start();
}

private static class Thread1 extends Thread {
public void run() {
synchronized (object1) {
System.out.println("Thread 1: Holds object 1.");

try {
Thread.sleep(10);
} catch (Exception e) {}
System.out.println("Thread 1: Waiting for object 2.");

synchronized (object2) {
System.out.println("Thread 1: Holds object 1 and object 2.");
}
}
}
}

private static class Thread2 extends Thread {
public void run() {
synchronized (object2) {
System.out.println("Thread 2: Holds object 2.");

try {
Thread.sleep(10);
} catch (Exception e) {}
System.out.println("Thread 2: Waiting for object 1.");

synchronized (object1) {
System.out.println("Thread 2: Holds object 1 and object 2.");
}
}
}
}
}

El código anterior nos dará un resultado similar a este:


Thread 1: Holds object 1.
Thread 2: Holds object 2.
Thread 1: Waiting for object 2.
Thread 2: Waiting for object 1.
 

Y eso es porque el primer hilo está usando el objet1 y esperando el objet2, pero el segundo hilo está usando el objet2 y no lo liberará hasta que también haya usado el objet1. Este es un caso claro de hilos bloqueados para siempre.

 

Solución simple para el problema de Deadlock

Cambiar el orden de los objetos evita que el programa entre en una situación de interbloqueo. Veamos el código:


public class DeadLockThreadSolution {

public static Object object1 = new Object();
public static Object object2 = new Object();

public static void main(String args[]) {
Thread1 thread1 = new Thread1();
Thread2 thread2 = new Thread2();
thread1.start();
thread2.start();
}

private static class Thread1 extends Thread {
public void run() {
synchronized (object1) {
System.out.println("Thread 1: Holds object 1.");

try {
Thread.sleep(10);
} catch (Exception e) {}
System.out.println("Thread 1: Waiting for object 2.");

synchronized (object2) {
System.out.println("Thread 1: Holds object 1 and object 2.");
}
}
}
}
private static class Thread2 extends Thread {
public void run() {
synchronized (object1) {
System.out.println("Thread 2: Holds object 1.");

try {
Thread.sleep(10);
} catch (Exception e) {}
System.out.println("Thread 2: Waiting for object 2.");

synchronized (object2) {
System.out.println("Thread 2: Holds object 1 and object 2.");
}
}
}
}
}

La salida será similar a ésta:


Thread 1: Holds object 1.
Thread 1: Waiting for object 2.
Thread 1: Holds object 1 and object 2.
Thread 2: Holds object 2.
Thread 2: Waiting for object 1.
Thread 2: Holds object 1 and object 2.

 

Problema de Inanición

El problema de la inanición ocurre cuando a un hilo se le niega continuamente el acceso a los recursos y, como resultado, no puede avanzar. Esto suele ocurrir cuando los subprocesos codiciosos consumen recursos compartidos durante largos períodos de tiempo. Repasemos el código:


public class StarvationThread {

private static Object object = new Object();
private static volatile boolean isActive = true;

public static void main(String[] args) {

Thread thread1 = new Thread(new Resource(), "Thread 1 - Priority 10");
Thread thread2 = new Thread(new Resource(), "Thread 2 - Priority 8");
Thread thread3 = new Thread(new Resource(), "Thread 3 - Priority 5");
Thread thread4 = new Thread(new Resource(), "Thread 4 - Priority 3");
Thread thread5 = new Thread(new Resource(), "Thread 5 - Priority 1");

thread1.setPriority(10);
thread2.setPriority(8);
thread3.setPriority(5);
thread4.setPriority(3);
thread5.setPriority(1);

thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();

try {
Thread.sleep(5000);
} catch (Exception e) {}

isActive = false;
}

private static class Resource implements Runnable {

private AtomicInteger resourceUsed = new AtomicInteger();

public void run() {
while (isActive) {
synchronized (object) {
System.out.format("%s: Resource has be used %d times.\n", Thread.currentThread().getName(), resourceUsed.getAndIncrement());
}
}

System.out.format("\n->%s: Resource was used %d times.\n\n", Thread.currentThread().getName(), resourceUsed.get());
}
}
}

 

El siguiente código nos dará una salida similar a esta:


Thread 1 - Priority 10: Resource was used 40652 times.
Thread 2 - Priority 8: Resource was used 30407 times.
Thread 3 - Priority 5: Resource was used 64029 times.
Thread 4 - Priority 3: Resource was used 10014 times.
Thread 5 - Priority 1: Resource was used 546 times.
 

Como puede ver, al hilo # 5 se le ha negado el acceso a los recursos y debido a esto, el progreso realizado es mínimo.

 

Solución simple para el problema de inanición

En general, se recomienda no modificar las prioridades de los hilos, ya que esta es la principal causa del problema de Inanición.

 

Problema de carrera

El problema Race ocurre cuando un subproceso va a actuar de acuerdo con una condición de verificación, pero otro subproceso puede haber intercalado y cambiado el valor del campo. Ahora, el primer hilo actuará en función de un valor que ya no es válido. Repasemos el código:



public class RaceThread {

private int number;

public static void main(String[] args) {

final RaceThread raceThread = new RaceThread();

for (int i = 1; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
raceThread.changeNumber();
}
}, "Thread " + i).start();
}
}

public void changeNumber() {
if (number == 0) {
number = 1;
System.out.println(Thread.currentThread().getName() + " - changed the number.");
} else {
System.out.println(Thread.currentThread().getName() + " - did Not change the number.");
}
}
}

El resultado será algo similar a esto:


Thread 13 - changed the number.
Thread 17 - changed the number.
Thread 35 - did Not change the number.
Thread 10 - changed the number.
Thread 48 - did Not change the number.
Thread 14 - changed the number.
Thread 60 - did Not change the number.
Thread 6 - changed the number.
Thread 5 - changed the number.
Thread 63 - did Not change the number.
Thread 18 - did Not change the number.

Algunos de los hilos cambiaron el valor del número y otros hilos no lo cambiaron. Esa es la razón por la que el número de uso de una condición no es válido en este código.

 

Solución simple para el problema de carrera

Una forma sencilla de corregir esto es utilizar la sincronización. La sincronización garantiza que solo un subproceso pueda acceder al recurso en un momento dado. Veamos el código:

 


public class RaceThreadSolution {

private int number;

public static void main(String[] args) {

final RaceThread raceThread = new RaceThread();

for (int i = 1; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
raceThread.changeNumber();
}
}, "Thread " + i).start();
}
}

public synchronized void changeNumber() {
if (number == 0) {
number = 1;
System.out.println(Thread.currentThread().getName() + " - changed the number.");
} else {
System.out.println(Thread.currentThread().getName() + " - did Not change the number.");
}
}

}

En este caso, solo el primer hilo cambió el valor de número y lo mantuvo válido para su uso en una condición.


T1 - changed the number.
T54 - did Not change the number.
T53 - did Not change the number.
T62 - did Not change the number.
T52 - did Not change the number.
T71 - did Not change the number.

 

Conclusión

Siempre hay una solución para cada problema. Solo necesita tomarse un momento, revisar su código y analizarlo para cualquiera que sea su objetivo final. Los hilos son una herramienta poderosa que puede hacernos la vida súper fácil como ingenieros, pero recuerde que un gran poder conlleva una gran responsabilidad.

Contenido

Categorías

Compartir Artículo

Artículos Destacados