A Segurança de Threads em Java é um tópico muito importante. Java oferece suporte a ambientes multi-threaded 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 inconsistência de dados quando as threads são usadas para ler e atualizar os dados compartilhados.
Segurança de Threads
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 por 1 quatro vezes e, como temos duas threads, seu valor deveria ser 8 após ambas as threads terminarem a execução. Mas ao executar o programa acima várias vezes, você notará que o valor de count varia entre 6, 7, 8. Isso ocorre porque, mesmo que count++ pareça ser uma operação atômica, não é e está causando corrupção de dados.
Segurança de Threads em Java
A segurança de threads em Java é o processo de tornar nosso programa seguro para uso em ambientes multithread, existem diferentes maneiras pelas quais podemos tornar nosso programa seguro para threads.
- A sincronização é a ferramenta mais fácil e amplamente utilizada para garantir a segurança de threads em Java.
- Uso das classes de invólucro atômico do pacote java.util.concurrent.atomic. Por exemplo, AtomicInteger
- Uso de bloqueios do pacote java.util.concurrent.locks.
- Uso de classes de coleção seguras para threads, consulte este post para o uso do ConcurrentHashMap para garantia de segurança de threads.
- Uso da palavra-chave volátil com variáveis para garantir que cada thread leia os dados da memória, não da 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 de cada vez. A palavra-chave synchronized do Java é usada para criar código sincronizado e internamente utiliza bloqueios em objetos ou classes para garantir que apenas uma thread esteja executando o código sincronizado.
- Java synchronization funciona bloqueando e desbloqueando o recurso antes que qualquer thread entre no código sincronizado. Antes de entrar no código sincronizado, é necessário adquirir o bloqueio no objeto, e quando a execução do código termina, ele desbloqueia o recurso que pode ser bloqueado por outras threads. Enquanto isso, outras threads estão 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 a outra é criar um bloco sincronizado.
- Quando um método é sincronizado, ele bloqueia o objeto; se o método for estático, ele bloqueia a classe. Portanto, é sempre uma boa prática usar um 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)
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, ele adquire um bloqueio em todos os campos do objeto.
- A sincronização em Java fornece integridade de dados ao custo de desempenho, portanto, deve ser usada apenas quando absolutamente necessário.
- A sincronização em Java funciona apenas na mesma JVM, portanto, se você precisar bloquear algum recurso em um ambiente com várias JVMs, não funcionará e você precisará procurar algum mecanismo de bloqueio global.
- Java Synchronization pode resultar em deadlocks, verifique este post sobre deadlock 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 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 na mesma String, ele tentará adquirir o bloqueio no mesmo objeto de referência de pool de String 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 no monitor do objeto
public synchronized void doSomething() {
// ...
}
}
// Código de hackers
MyObject myObject = new MyObject();
synchronized (myObject) {
while (true) {
// Atraso indefinido em myObject
Thread.sleep(Integer.MAX_VALUE);
}
}
Observe que o código do hacker está tentando travar a instância myObject e, uma vez que obtém o bloqueio, nunca o libera, fazendo com que o método doSomething() bloqueie aguardando o bloqueio, isso causará o sistema a entrar em um impasse e causar uma Negação 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 o bloco sincronizado em paralelo em vários threads. Um caso semelhante é verdadeiro se você tiver um objeto privado, mas tiver um método setter para alterar sua referência.
public class MyObject {
//bloqueia o monitor do objeto da classe
public static synchronized void doSomething() {
// ...
}
}
//código de hackers
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, isso causará um impasse e DoS no sistema. Aqui está outro exemplo em que vários threads estão trabalhando no mesmo array de Strings e, uma vez processados, adicionando o nome do 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 todos os threads
t1.start();t2.start();t3.start();
//aguardar threads terminarem
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 adicionar nome do 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 addThreadName() para tornar nosso programa thread-safe.
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 bem 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 threads em Java, espero que você tenha aprendido sobre programação thread-safe e o uso da palavra-chave synchronized.
Source:
https://www.digitalocean.com/community/tutorials/thread-safety-in-java