Comprender los tipos de errores de sincronización de subprocesos en Java

Comprender los tipos de errores de sincronización de subprocesos en Java

El contenido de Developer.com y las recomendaciones de productos son editorialmente independientes. Podemos ganar dinero cuando hace clic en enlaces a nuestros socios. Aprende más.

Tutoriales de programación Java

Multithreading es un concepto poderoso en Java que permite a los programas ejecutar múltiples threads simultáneamente. Sin embargo, esta capacidad impone al desarrollador la responsabilidad de gestionar la sincronización, garantizando que los subprocesos no interfieran entre sí y produzcan resultados inesperados. Los errores de sincronización de subprocesos pueden ser difíciles de detectar y difíciles de detectar, lo que los convierte en una fuente común de errores en aplicaciones Java multiproceso. Este tutorial describe los distintos tipos de errores de sincronización de subprocesos y ofrece sugerencias para solucionarlos.

Salta a:

Condiciones de carrera

A carrera La condición ocurre cuando el comportamiento de un programa depende del tiempo relativo de los eventos, como el orden en el que se programa la ejecución de los subprocesos. Esto puede provocar resultados impredecibles y corrupción de datos. Considere el siguiente ejemplo:

public class RaceConditionExample {

    private static int counter = 0;


    public static void main(String[] args) {

        Runnable incrementTask = () -> {

            for (int i = 0; i < 10000; i++) {

                counter++;

            }

        };

        Thread thread1 = new Thread(incrementTask);

        Thread thread2 = new Thread(incrementTask);

        thread1.start();

        thread2.start();

        try {

            thread1.join();

            thread2.join();

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        System.out.println("Counter: " + counter);

    }

}

En este ejemplo, dos subprocesos incrementan una variable de contador compartida. Debido a la falta de sincronización, se produce una condición de carrera y el valor final del contador es impredecible. Para solucionar este problema podemos utilizar el sincronizado palabra clave:

public class FixedRaceConditionExample {

    private static int counter = 0;

    public static synchronized void increment() {

        for (int i = 0; i < 10000; i++) {

            counter++;

        }

    }

    public static void main(String[] args) {

        Thread thread1 = new Thread(FixedRaceConditionExample::increment);

        Thread thread2 = new Thread(FixedRaceConditionExample::increment);

        thread1.start();

        thread2.start();

        try {

            thread1.join();

            thread2.join();

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        System.out.println("Counter: " + counter);

    }

}

Utilizando el sincronizado palabra clave en el incremento El método garantiza que solo un subproceso pueda ejecutarlo a la vez, evitando así la condición de carrera.

Detectar condiciones de carrera requiere un análisis cuidadoso de su código y comprender las interacciones entre subprocesos. Utilice siempre mecanismos de sincronización, como sincronizado métodos o bloques, para proteger los recursos compartidos y evitar condiciones de carrera.

Puntos muertos

Puntos muertos Ocurre cuando dos o más subprocesos están bloqueados para siempre, cada uno esperando a que el otro libere un bloqueo. Esta situación puede paralizar su solicitud. Consideremos un ejemplo clásico de punto muerto:

public class DeadlockExample {

    private static final Object lock1 = new Object();

    private static final Object lock2 = new Object();

    public static void main(String[] args) {

        Thread thread1 = new Thread(() -> {

            synchronized (lock1) {

                System.out.println("Thread 1: Holding lock 1");

                try {

                    Thread.sleep(100);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

                System.out.println("Thread 1: Waiting for lock 2");

                synchronized (lock2) {

                    System.out.println("Thread 1: Holding lock 1 and lock 2");

                }

            }

        });

        Thread thread2 = new Thread(() -> {

            synchronized (lock2) {

                System.out.println("Thread 2: Holding lock 2");

                try {

                    Thread.sleep(100);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

                System.out.println("Thread 2: Waiting for lock 1");

                synchronized (lock1) {

                    System.out.println("Thread 2: Holding lock 2 and lock 1");

                }

            }

        });

        thread1.start();

        thread2.start();

    }

}

En este ejemplo, Hilo 1 sostiene cerradura1 y espera cerradura2mientras Hilo 2 sostiene cerradura2 y espera cerradura1. Esto da como resultado un punto muerto, ya que ninguno de los subprocesos puede continuar.

Para evitar interbloqueos, asegúrese de que los subprocesos siempre adquieran bloqueos en el mismo orden. Si se necesitan varios candados, utilice un orden coherente para adquirirlos. Aquí hay una versión modificada del ejemplo anterior que evita el punto muerto:

public class FixedDeadlockExample {

    private static final Object lock1 = new Object();

    private static final Object lock2 = new Object();

    public static void main(String[] args) {

        Thread thread1 = new Thread(() -> {

            synchronized (lock1) {

                System.out.println("Thread 1: Holding lock 1");

                try {

                    Thread.sleep(100);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

                System.out.println("Thread 1: Waiting for lock 2");

                synchronized (lock2) {

                    System.out.println("Thread 1: Holding lock 2");

                }

            }

        });

        Thread thread2 = new Thread(() -> {

            synchronized (lock1) {

                System.out.println("Thread 2: Holding lock 1");

                try {

                    Thread.sleep(100);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

                System.out.println("Thread 2: Waiting for lock 2");

                synchronized (lock2) {

                    System.out.println("Thread 2: Holding lock 2");

                }

            }

        });

        thread1.start();

        thread2.start();

    }

}

En esta versión fija, ambos hilos adquieren bloqueos en el mismo orden: primero cerradura1entonces cerradura2. Esto elimina la posibilidad de un punto muerto.

La prevención de interbloqueos implica un diseño cuidadoso de su estrategia de bloqueo. Adquiera siempre los bloqueos en un orden coherente para evitar dependencias circulares entre subprocesos. Utilice herramientas como volcados de subprocesos y perfiladores para identificar y resolver problemas de interbloqueo en sus programas Java. Además, considere leer nuestro tutorial sobre Cómo prevenir interbloqueos de subprocesos en Java para conocer aún más estrategias.

Inanición

Inanición ocurre cuando un hilo no puede obtener acceso regular a recursos compartidos y no puede progresar. Esto puede suceder cuando un subproceso con una prioridad más baja es constantemente reemplazado por subprocesos con prioridades más altas. Considere el siguiente ejemplo de código:

public class StarvationExample {

    private static final Object lock = new Object();

    public static void main(String[] args) {

        Thread highPriorityThread = new Thread(() -> {

            while (true) {

                synchronized (lock) {

                    System.out.println("High Priority Thread is working");

                }

            }

        });

        Thread lowPriorityThread = new Thread(() -> {

            while (true) {

                synchronized (lock) {

                    System.out.println("Low Priority Thread is working");

                }

            }

        });

        highPriorityThread.setPriority(Thread.MAX_PRIORITY);

        lowPriorityThread.setPriority(Thread.MIN_PRIORITY);

        highPriorityThread.start();

        lowPriorityThread.start();

    }

}


En este ejemplo, tenemos un hilo de alta prioridad y un hilo de baja prioridad, ambos compitiendo por un bloqueo. El hilo de alta prioridad domina y el hilo de baja prioridad se muere de hambre.

Para mitigar la inanición, puede utilizar bloqueos justos o ajustar las prioridades de los hilos. Aquí hay una versión actualizada usando un Bloqueo reentrante con el justicia bandera habilitada:

import java.util.concurrent.locks.Lock;

import java.util.concurrent.locks.ReentrantLock;


public class FixedStarvationExample {

    // The true boolean value enables fairness

    private static final Lock lock = new ReentrantLock(true);

    public static void main(String[] args) {

        Thread highPriorityThread = new Thread(() -> {

            while (true) {

                lock.lock();

                try {

                    System.out.println("High Priority Thread is working");

                } finally {

                    lock.unlock();

                }

            }

        });

        Thread lowPriorityThread = new Thread(() -> {

            while (true) {

                lock.lock();

                try {

                    System.out.println("Low Priority Thread is working");

                } finally {

                    lock.unlock();

                }

            }

        });

        highPriorityThread.setPriority(Thread.MAX_PRIORITY);

        lowPriorityThread.setPriority(Thread.MIN_PRIORITY);

        highPriorityThread.start();

        lowPriorityThread.start();

    }

}

El Bloqueo reentrante con justicia garantiza que el hilo que lleva más tiempo esperando obtenga el bloqueo, lo que reduce la probabilidad de morir de hambre.

Mitigar la hambruna implica considerar cuidadosamente las prioridades de los subprocesos, utilizar bloqueos justos y garantizar que todos los subprocesos tengan acceso equitativo a los recursos compartidos. Revise y ajuste periódicamente las prioridades de sus subprocesos según los requisitos de su aplicación.

Consulte nuestro tutorial sobre las mejores prácticas de subprocesamiento para aplicaciones Java.

Inconsistencia de datos

Inconsistencia de datos Ocurre cuando varios subprocesos acceden a datos compartidos sin una sincronización adecuada, lo que genera resultados inesperados e incorrectos. Considere el siguiente ejemplo:

public class DataInconsistencyExample {

    private static int sharedValue = 0;

    public static void main(String[] args) {

        Runnable incrementTask = () -> {

            for (int i = 0; i < 1000; i++) {

                sharedValue++;

            }

        };

        Thread thread1 = new Thread(incrementTask);

        Thread thread2 = new Thread(incrementTask);

        thread1.start();

        thread2.start();

        try {

            thread1.join();

            thread2.join();

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        System.out.println("Shared Value: " + sharedValue);

    }

}

En este ejemplo, dos subprocesos incrementan un valor compartido sin sincronización. Como resultado, el valor final del valor compartido es impredecible e inconsistente.

Para solucionar problemas de inconsistencia de datos, puede utilizar el sincronizado palabra clave u otros mecanismos de sincronización:

public class FixedDataInconsistencyExample {

    private static int sharedValue = 0;


    public static synchronized void increment() {

        for (int i = 0; i < 1000; i++) {

            sharedValue++;

        }

    }

    public static void main(String[] args) {

        Thread thread1 = new Thread(FixedDataInconsistencyExample::increment);

        Thread thread2 = new Thread(FixedDataInconsistencyExample::increment);

        thread1.start();

        thread2.start();

        try {

            thread1.join();

            thread2.join();

        } catch (InterruptedException e) {

            e.printStackTrace();

        }
        System.out.println("Shared Value: " + sharedValue);

    }

}

Utilizando el sincronizado palabra clave en el incremento El método garantiza que solo un subproceso pueda ejecutarlo a la vez, evitando la inconsistencia de los datos.

Para evitar la inconsistencia de los datos, sincronice siempre el acceso a los datos compartidos. Utilizar el sincronizado palabra clave u otros mecanismos de sincronización para proteger secciones críticas de código. Revise periódicamente su código para detectar posibles problemas de inconsistencia de datos, especialmente en entornos multiproceso.

Reflexiones finales sobre la detección y reparación de errores de sincronización de subprocesos en Java

En este tutorial de Java, exploramos ejemplos prácticos de cada tipo de error de sincronización de subprocesos y proporcionamos soluciones para solucionarlos. Los errores de sincronización de subprocesos, como condiciones de carrera, interbloqueos, inanición e inconsistencia de datos, pueden introducir errores sutiles y difíciles de encontrar. Sin embargo, al incorporar las estrategias presentadas aquí en su código Java, puede mejorar la estabilidad y el rendimiento de sus aplicaciones multiproceso.

Leer: Los mejores cursos en línea para Java


Source link

About Carlos Carraveo Jimenez

Check Also

La clave para gestionar un sistema sanitario seguro y eficiente |

La clave para gestionar un sistema sanitario seguro y eficiente |

Ejemplos del mundo real Numerosas instituciones de atención médica han implementado con éxito servicios administrados …

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *