Segurança de Threads em Java

A segurança de thread em Java é um tópico muito importante. O Java oferece suporte a ambientes multithread usando Threads em Java. Sabemos que várias threads criadas a partir do mesmo objeto compartilham as variáveis do objeto e isso pode levar a uma inconsistência de dados quando as threads são usadas para ler e atualizar os dados compartilhados.

Segurança de Thread

A razão para a inconsistência de dados é porque a atualização de qualquer valor de campo não é um processo atômico, requer três etapas; primeiro, ler o valor atual, segundo, realizar as operações necessárias para obter o valor atualizado e terceiro, atribuir o valor atualizado à referência do campo. Vamos verificar isso com um programa simples onde várias threads estão atualizando os dados compartilhados.

package com.journaldev.threads;

public class ThreadSafety {

    public static void main(String[] args) throws InterruptedException {
    
        ProcessingThread pt = new ProcessingThread();
        Thread t1 = new Thread(pt, "t1");
        t1.start();
        Thread t2 = new Thread(pt, "t2");
        t2.start();
        //aguardar as threads terminarem o processamento
        t1.join();
        t2.join();
        System.out.println("Processing count="+pt.getCount());
    }

}

class ProcessingThread implements Runnable{
    private int count;
    
    @Override
    public void run() {
        for(int i=1; i < 5; i++){
            processSomething(i);
        	count++;
        }
    }

    public int getCount() {
        return this.count;
    }

    private void processSomething(int i) {
        //processando algum trabalho
        try {
            Thread.sleep(i*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}

No programa acima, no loop for, count é incrementado 1 quatro vezes e, como temos duas threads, seu valor deveria ser 8 depois que ambas as threads terminassem a execução. Mas quando você executar o programa acima várias vezes, você notará que o valor de count varia entre 6, 7, 8. Isso acontece porque mesmo se count++ parecer ser uma operação atômica, ela NÃO é e está causando corrupção de dados.

Segurança de Threads em Java

A segurança de threads em Java é o processo para tornar nosso programa seguro para uso em um ambiente multithread, existem diferentes maneiras pelas quais podemos tornar nosso programa thread-safe.

  • A sincronização é a ferramenta mais fácil e amplamente utilizada para segurança de threads em Java.
  • Uso das classes Atomic Wrapper do pacote java.util.concurrent.atomic. Por exemplo, AtomicInteger
  • Uso de locks do pacote java.util.concurrent.locks.
  • Uso de classes de coleção thread-safe, verifique este post para o uso de ConcurrentHashMap para segurança de threads.
  • Uso da palavra-chave volatile com variáveis para fazer com que cada thread leia os dados da memória, não da memória cache da thread.

Java synchronized

A sincronização é a ferramenta por meio da qual podemos alcançar a segurança de threads, a JVM garante que o código sincronizado será executado por apenas uma thread por vez. A palavra-chave synchronized do Java é usada para criar código sincronizado e internamente ela usa locks em Objeto ou Classe para garantir que apenas uma thread esteja executando o código sincronizado.

  • Java synchronization trabalha com o bloqueio e desbloqueio do recurso antes que qualquer thread entre no código sincronizado, ela precisa adquirir o bloqueio no objeto e quando a execução do código termina, ela desbloqueia o recurso que pode ser bloqueado por outras threads. Enquanto isso, outras threads ficam em estado de espera para bloquear o recurso sincronizado.
  • Podemos usar a palavra-chave synchronized de duas maneiras, uma é tornar um método completo sincronizado e outra maneira é criar um bloco sincronizado.
  • Quando um método é sincronizado, ele bloqueia o Objeto, se o método for estático, ele bloqueia a Classe, então sempre é uma boa prática usar o bloco sincronizado para bloquear apenas as seções do método que precisam de sincronização.
  • Ao criar um bloco sincronizado, precisamos fornecer o recurso no qual o bloqueio será adquirido, pode ser XYZ.class ou qualquer campo de objeto da classe.
  • synchronized(this) irá bloquear o Objeto antes de entrar no bloco sincronizado.
  • Você deve usar o nível mais baixo de bloqueio, por exemplo, se houver vários blocos sincronizados em uma classe e um deles estiver bloqueando o Objeto, os outros blocos sincronizados também não estarão disponíveis para execução por outras threads. Quando bloqueamos um Objeto, adquirimos um bloqueio em todos os campos do Objeto.
  • A sincronização em Java garante integridade de dados com o custo de desempenho, portanto, deve ser usada apenas quando absolutamente necessário.
  • A sincronização em Java funciona apenas na mesma JVM, então se você precisar bloquear algum recurso em um ambiente com várias JVMs, não funcionará e você pode precisar procurar por algum mecanismo de bloqueio global.
  • Java Synchronization pode resultar em impasses, confira este post sobre impasse em Java e como evitá-los.
  • A palavra-chave sincronizada do Java não pode ser usada para construtores e variáveis.
  • É preferível criar um objeto privado fictício para usar no bloco sincronizado, para que sua referência não possa ser alterada por nenhum outro código. Por exemplo, se você tiver um método setter para o objeto no qual está sincronizando, sua referência pode ser alterada por algum outro código, levando à execução paralela do bloco sincronizado.
  • Não devemos usar nenhum objeto que seja mantido em um pool constante, por exemplo, String não deve ser usado para sincronização, porque se algum outro código também estiver bloqueando a mesma String, ele tentará adquirir o bloqueio no mesmo objeto de referência do pool de Strings e, mesmo que ambos os códigos não estejam relacionados, eles se bloquearão mutuamente.

Aqui estão as alterações de código que precisamos fazer no programa acima para torná-lo thread-safe.

    // variável de objeto fictício para sincronização
    private Object mutex=new Object();
    ...
    // usando bloco sincronizado para ler, incrementar e atualizar o valor do contador de forma síncrona
    synchronized (mutex) {
            count++;
    }

Vamos ver alguns exemplos de sincronização e o que podemos aprender com eles.

public class MyObject {
 
  // Bloqueia o monitor do objeto
  public synchronized void doSomething() { 
    // ...
  }
}
 
// Código de hackers
MyObject myObject = new MyObject();
synchronized (myObject) {
  while (true) {
    // Atraso indefinido do meuObjeto
    Thread.sleep(Integer.MAX_VALUE); 
  }
}

Notifique que o código do hacker está tentando travar a instância do meuObjeto e, uma vez que obtém o bloqueio, nunca o libera, causando o bloqueio do método façaAlgo() ao esperar pelo bloqueio. Isso provocará um impasse no sistema e causará uma Negativa de Serviço (DoS).

public class MyObject {
  public Object lock = new Object();
 
  public void doSomething() {
    synchronized (lock) {
      // ...
    }
  }
}

//código não confiável

MyObject myObject = new MyObject();
//alterar a referência do objeto de bloqueio
myObject.lock = new Object();

Observe que o objeto de bloqueio é público e, ao alterar sua referência, podemos executar um bloco sincronizado em paralelo em várias threads. Um caso semelhante ocorre se você tiver um objeto privado, mas tiver um método setter para alterar sua referência.

public class MyObject {
  //trava no monitor do objeto da classe
  public static synchronized void doSomething() { 
    // ...
  }
}
 
//código do hacker
synchronized (MyObject.class) {
  while (true) {
    Thread.sleep(Integer.MAX_VALUE); // Indefinitely delay MyObject
  }
}

Observe que o código do hacker está obtendo um bloqueio no monitor da classe e não o liberando, o que causará um impasse e DoS no sistema. Aqui está outro exemplo em que várias threads estão trabalhando no mesmo array de Strings e, uma vez processado, anexando o nome da thread ao valor do array.

package com.journaldev.threads;

import java.util.Arrays;

public class SyncronizedMethod {

    public static void main(String[] args) throws InterruptedException {
        String[] arr = {"1","2","3","4","5","6"};
        HashMapProcessor hmp = new HashMapProcessor(arr);
        Thread t1=new Thread(hmp, "t1");
        Thread t2=new Thread(hmp, "t2");
        Thread t3=new Thread(hmp, "t3");
        long start = System.currentTimeMillis();
        //iniciar todas as threads
        t1.start();t2.start();t3.start();
        //aguardar o término das threads
        t1.join();t2.join();t3.join();
        System.out.println("Time taken= "+(System.currentTimeMillis()-start));
        //verificar o valor da variável compartilhada agora
        System.out.println(Arrays.asList(hmp.getMap()));
    }

}

class HashMapProcessor implements Runnable{
    
    private String[] strArr = null;
    
    public HashMapProcessor(String[] m){
        this.strArr=m;
    }
    
    public String[] getMap() {
        return strArr;
    }

    @Override
    public void run() {
        processArr(Thread.currentThread().getName());
    }

    private void processArr(String name) {
        for(int i=0; i < strArr.length; i++){
            //processar dados e anexar o nome da thread
            processSomething(i);
            addThreadName(i, name);
        }
    }
    
    private void addThreadName(int i, String name) {
        strArr[i] = strArr[i] +":"+name;
    }

    private void processSomething(int index) {
        //processando algum trabalho
        try {
            Thread.sleep(index*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}

Aqui está a saída quando executo o programa acima.

Time taken= 15005
[1:t2:t3, 2:t1, 3:t3, 4:t1:t3, 5:t2:t1, 6:t3]

Os valores do array de Strings estão corrompidos devido aos dados compartilhados e à falta de sincronização. Aqui está como podemos alterar o método adicionarNomeThread() para tornar nosso programa seguro para threads.

    private Object lock = new Object();
    private void addThreadName(int i, String name) {
        synchronized(lock){
        strArr[i] = strArr[i] +":"+name;
        }
    }

Após essa alteração, nosso programa funciona corretamente e aqui está a saída correta do programa.

Time taken= 15004
[1:t1:t2:t3, 2:t2:t1:t3, 3:t2:t3:t1, 4:t3:t2:t1, 5:t2:t1:t3, 6:t2:t1:t3]

Isso é tudo sobre segurança de thread em Java. Espero que você tenha aprendido sobre programação segura para threads e o uso da palavra-chave synchronized.

Source:
https://www.digitalocean.com/community/tutorials/thread-safety-in-java