Multithreading é um conceito poderoso em Java, permitindo que programas executem vários threads simultaneamente. No entanto, essa capacidade coloca o ônus de gerenciar a sincronização, garantindo que os threads não interfiram uns com os outros e produzam resultados inesperados, no desenvolvedor. Erros de sincronização de threads podem ser evasivos e desafiadores de detectar, tornando-os uma fonte comum de bugs em aplicativos Java multithread. Este tutorial descreve os vários tipos de erros de sincronização de threads e oferece sugestões para corrigi-los.
Ir para:
Condições de corrida
UM corrida condição ocorre quando o comportamento de um programa depende do tempo relativo de eventos, como a ordem em que os threads são agendados para execução. Isso pode levar a resultados imprevisíveis e corrupção de dados. Considere o seguinte exemplo:
public class RaceConditionExample { non-public static int counter = 0; public static void principal(String() args) { Runnable incrementTask = () -> { for (int i = 0; i < 10000; i++) { counter++; } }; Thread thread1 = new Thread(incrementTask); Thread thread2 = new Thread(incrementTask); thread1.begin(); thread2.begin(); strive { thread1.be a part of(); thread2.be a part of(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Counter: " + counter); } }
Neste exemplo, duas threads estão incrementando uma variável de contador compartilhada. Devido à falta de sincronização, ocorre uma condição de corrida, e o valor ultimate do contador é imprevisível. Para corrigir isso, podemos usar o sincronizado palavra-chave:
public class FixedRaceConditionExample { non-public static int counter = 0; public static synchronized void increment() { for (int i = 0; i < 10000; i++) { counter++; } } public static void principal(String() args) { Thread thread1 = new Thread(FixedRaceConditionExample::increment); Thread thread2 = new Thread(FixedRaceConditionExample::increment); thread1.begin(); thread2.begin(); strive { thread1.be a part of(); thread2.be a part of(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Counter: " + counter); } }
Usando o sincronizado palavra-chave no incremento O método garante que apenas uma thread possa executá-lo por vez, evitando assim a condição de corrida.
Detectar condições de corrida requer uma análise cuidadosa do seu código e a compreensão das interações entre threads. Sempre use mecanismos de sincronização, como sincronizado métodos ou blocos, para proteger recursos compartilhados e evitar condições de corrida.
Impasses
Impasses ocorrem quando dois ou mais threads são bloqueados para sempre, cada um esperando que o outro libere um bloqueio. Essa situação pode paralisar seu aplicativo. Vamos considerar um exemplo clássico de um impasse:
public class DeadlockExample { non-public static ultimate Object lock1 = new Object(); non-public static ultimate Object lock2 = new Object(); public static void principal(String() args) { Thread thread1 = new Thread(() -> { synchronized (lock1) { System.out.println("Thread 1: Holding lock 1"); strive { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread 1: Ready 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"); strive { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread 2: Ready for lock 1"); synchronized (lock1) { System.out.println("Thread 2: Holding lock 2 and lock 1"); } } }); thread1.begin(); thread2.begin(); } }
Neste exemplo, Tópico 1 segura fechadura1 e espera por fechadura2enquanto Tópico 2 segura fechadura2 e espera por fechadura1. Isso resulta em um impasse, pois nenhuma thread pode prosseguir.
Para evitar deadlocks, garanta que os threads sempre adquiram os locks na mesma ordem. Se vários locks forem necessários, use uma ordem consistente para adquiri-los. Aqui está uma versão modificada do exemplo anterior que evita o impasse:
public class FixedDeadlockExample { non-public static ultimate Object lock1 = new Object(); non-public static ultimate Object lock2 = new Object(); public static void principal(String() args) { Thread thread1 = new Thread(() -> { synchronized (lock1) { System.out.println("Thread 1: Holding lock 1"); strive { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread 1: Ready 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"); strive { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread 2: Ready for lock 2"); synchronized (lock2) { System.out.println("Thread 2: Holding lock 2"); } } }); thread1.begin(); thread2.begin(); } }
Nesta versão fixa, ambos os threads adquirem bloqueios na mesma ordem: primeiro fechadura1então fechadura2. Isso elimina a possibilidade de um deadlock.
Prevenir deadlocks envolve um design cuidadoso da sua estratégia de bloqueio. Sempre adquira bloqueios em uma ordem consistente para evitar dependências circulares entre threads. Use ferramentas como dumps de thread e profilers para identificar e resolver problemas de impasse em seus programas Java. Além disso, considere ler nosso tutorial sobre Como evitar deadlocks de thread em Java para ainda mais estratégias.
Fome
Fome ocorre quando um thread não consegue obter acesso common a recursos compartilhados e não consegue progredir. Isso pode acontecer quando um thread com prioridade mais baixa é constantemente preemptado por threads com prioridades mais altas. Considere o seguinte exemplo de código:
public class StarvationExample { non-public static ultimate Object lock = new Object(); public static void principal(String() args) { Thread highPriorityThread = new Thread(() -> { whereas (true) { synchronized (lock) { System.out.println("Excessive Precedence Thread is working"); } } }); Thread lowPriorityThread = new Thread(() -> { whereas (true) { synchronized (lock) { System.out.println("Low Precedence Thread is working"); } } }); highPriorityThread.setPriority(Thread.MAX_PRIORITY); lowPriorityThread.setPriority(Thread.MIN_PRIORITY); highPriorityThread.begin(); lowPriorityThread.begin(); } }
Neste exemplo, temos um thread de alta prioridade e um thread de baixa prioridade, ambos disputando um bloqueio. O thread de alta prioridade domina, e o thread de baixa prioridade sofre inanição.
Para mitigar a fome, você pode usar bloqueios justos ou ajustar as prioridades de thread. Aqui está uma versão atualizada usando um Bloqueio Reentrante com o justiça sinalizador habilitado:
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class FixedStarvationExample { // The true boolean worth allows equity non-public static ultimate Lock lock = new ReentrantLock(true); public static void principal(String() args) { Thread highPriorityThread = new Thread(() -> { whereas (true) { lock.lock(); strive { System.out.println("Excessive Precedence Thread is working"); } lastly { lock.unlock(); } } }); Thread lowPriorityThread = new Thread(() -> { whereas (true) { lock.lock(); strive { System.out.println("Low Precedence Thread is working"); } lastly { lock.unlock(); } } }); highPriorityThread.setPriority(Thread.MAX_PRIORITY); lowPriorityThread.setPriority(Thread.MIN_PRIORITY); highPriorityThread.begin(); lowPriorityThread.begin(); } }
O Bloqueio Reentrante com justiça garante que o thread com maior espera obtenha o bloqueio, reduzindo a probabilidade de inanição.
A mitigação da fome envolve considerar cuidadosamente as prioridades dos threads, usar bloqueios justos e garantir que todos os threads tenham acesso equitativo aos recursos compartilhados. Revise e ajuste regularmente suas prioridades de threads com base nos requisitos do seu aplicativo.
Confira nosso tutorial sobre o Melhores práticas de threading para aplicativos Java.
Inconsistência de dados
Inconsistência de dados ocorre quando vários threads acessam dados compartilhados sem sincronização adequada, levando a resultados inesperados e incorretos. Considere o seguinte exemplo:
public class DataInconsistencyExample { non-public static int sharedValue = 0; public static void principal(String() args) { Runnable incrementTask = () -> { for (int i = 0; i < 1000; i++) { sharedValue++; } }; Thread thread1 = new Thread(incrementTask); Thread thread2 = new Thread(incrementTask); thread1.begin(); thread2.begin(); strive { thread1.be a part of(); thread2.be a part of(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Shared Worth: " + sharedValue); } }
Neste exemplo, dois threads estão incrementando um valor compartilhado sem sincronização. Como resultado, o valor ultimate do valor compartilhado é imprevisível e inconsistente.
Para corrigir problemas de inconsistência de dados, você pode usar o sincronizado palavra-chave ou outros mecanismos de sincronização:
public class FixedDataInconsistencyExample { non-public static int sharedValue = 0; public static synchronized void increment() { for (int i = 0; i < 1000; i++) { sharedValue++; } } public static void principal(String() args) { Thread thread1 = new Thread(FixedDataInconsistencyExample::increment); Thread thread2 = new Thread(FixedDataInconsistencyExample::increment); thread1.begin(); thread2.begin(); strive { thread1.be a part of(); thread2.be a part of(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Shared Worth: " + sharedValue); } }
Usando o sincronizado palavra-chave no incremento O método garante que apenas um thread possa executá-lo por vez, evitando inconsistência de dados.
Para evitar inconsistência de dados, sempre sincronize o acesso aos dados compartilhados. Use o sincronizado palavra-chave ou outros mecanismos de sincronização para proteger seções críticas do código. Revise regularmente seu código para possíveis problemas de inconsistência de dados, especialmente em ambientes multithread.
Considerações finais sobre a detecção e correção de erros de sincronização de threads em Java
Neste tutorial Java, exploramos exemplos práticos de cada tipo de erro de sincronização de thread e fornecemos soluções para corrigi-los. Erros de sincronização de thread, como condições de corrida, deadlocks, hunger e inconsistência de dados, podem introduzir bugs sutis e difíceis de encontrar. No entanto, ao incorporar as estratégias apresentadas aqui em seu código Java, você pode aprimorar a estabilidade e o desempenho de seus aplicativos multithread.