Introdução à sincronização em Java

A sincronização é um recurso Java que impede que vários encadeamentos tentem acessar os recursos comumente compartilhados ao mesmo tempo. Aqui, os recursos compartilhados se referem ao conteúdo do arquivo externo, variáveis ​​de classe ou registros do banco de dados.

A sincronização é amplamente usada em programação multithread. "Sincronizado" é a palavra-chave que fornece ao seu código a capacidade de permitir que apenas um único thread opere sem interferência de nenhum outro thread durante esse período.

Por que precisamos de sincronização em Java?

  • Java é uma linguagem de programação multithread. Isso significa que dois ou mais encadeamentos podem ser executados simultaneamente para a conclusão de uma tarefa. Quando os threads são executados simultaneamente, há grandes chances de ocorrer um cenário em que seu código possa fornecer resultados inesperados.
  • Você pode se perguntar que, se o multithreading pode causar resultados errôneos, por que é considerado um recurso importante em Java?
  • A multithreading torna seu código mais rápido executando vários threads em paralelo, reduzindo assim o tempo de execução dos códigos e proporcionando alto desempenho. No entanto, o uso do ambiente multithreading leva a resultados imprecisos devido a uma condição conhecida como condição de corrida.

O que é uma condição de corrida?

Quando dois ou mais encadeamentos são executados em paralelo, eles tendem a acessar e modificar recursos compartilhados naquele momento. As seqüências nas quais os threads são executados são decididas pelo algoritmo de planejamento de threads.

Devido a isso, não é possível prever a ordem na qual os threads serão executados, pois são controlados apenas pelo planejador de threads. Isso afeta a saída do código e resulta em saídas inconsistentes. Como vários threads estão competindo entre si para concluir a operação, a condição é chamada de "condição de corrida".

Por exemplo, vamos considerar o código abaixo:

Class Modify:
package JavaConcepts;
public class Modify implements Runnable(
private int myVar=0;
public int getMyVar() (
return myVar;
)
public void setMyVar(int myVar) (
this.myVar = myVar;
)
public void increment() (
myVar++;
)
@Override
public void run() (
// TODO Auto-generated method stub
this.increment();
System.out.println("Current thread being executed "+ Thread.currentThread().getName() + "Current Thread value " + this.getMyVar());
)
)
Class RaceCondition:
package JavaConcepts;
public class RaceCondition (
public static void main(String() args) (
Modify mObj = new Modify();
Thread t1 = new Thread(mObj, "thread 1");
Thread t2 = new Thread(mObj, "thread 2");
Thread t3 = new Thread(mObj, "thread 3");
t1.start();
t2.start();
t3.start();
)
)

Ao executar consecutivamente o código acima, as saídas serão as seguintes:

Ourput1:

Thread atual sendo executado thread 1 Valor atual do thread 3

Thread atual sendo executado thread 3 Valor atual do Thread 2

Thread atual sendo executado thread 2 Valor atual do thread 3

Saída2:

Thread atual sendo executado thread 3 Valor atual do thread 3

Thread atual sendo executado thread 2 Valor atual do thread 3

Thread atual sendo executado thread 1 Valor atual do thread 3

Saída3:

Thread atual sendo executado thread 2 Valor atual do thread 3

Thread atual sendo executado thread 1 Valor atual do thread 3

Thread atual sendo executado thread 3 Valor atual do thread 3

Saída4:

Thread atual sendo executado thread 1 Valor atual do thread 2

Thread atual sendo executado thread 3 Valor atual do thread 3

Thread atual sendo executado thread 2 Valor atual do thread 2

  • No exemplo acima, você pode concluir que os threads estão sendo executados aleatoriamente e também o valor está incorreto. Conforme nossa lógica, o valor deve ser incrementado em 1. No entanto, aqui o valor de saída na maioria dos casos é 3 e, em alguns casos, é 2.
  • Aqui, a variável "myVar" é o recurso compartilhado no qual vários threads estão executando. Os threads estão acessando e modificando o valor de "myVar" simultaneamente. Vamos ver o que acontece se comentarmos os outros dois tópicos.

A saída neste caso é:

O segmento atual que está sendo executado, o segmento 1 Valor do segmento atual 1

Isso significa que quando um único encadeamento está sendo executado, a saída é conforme o esperado. No entanto, quando vários threads estão em execução, o valor está sendo modificado por cada thread. Portanto, é necessário restringir o número de threads trabalhando em um recurso compartilhado a um único thread por vez. Isso é alcançado usando a sincronização.

Compreendendo o que é sincronização em Java

  • A sincronização em Java é alcançada com a ajuda da palavra-chave "sincronizado". Essa palavra-chave pode ser usada para métodos, blocos ou objetos, mas não pode ser usada com classes e variáveis. Um trecho de código sincronizado permite que apenas um encadeamento acesse e modifique-o em um determinado momento.
  • No entanto, um trecho de código sincronizado afeta o desempenho do código, pois aumenta o tempo de espera de outros threads que tentam acessá-lo. Portanto, um pedaço de código deve ser sincronizado apenas quando houver uma chance de ocorrer uma condição de corrida. Se não alguém deve evitá-lo.

Como a sincronização em Java funciona internamente?

  • A sincronização interna em Java foi implementada com a ajuda do conceito de bloqueio (também conhecido como monitor). Todo objeto Java possui seu próprio bloqueio. Em um bloco de código sincronizado, um encadeamento precisa adquirir o bloqueio antes de poder executar esse bloco de código específico. Depois que um thread adquire o bloqueio, ele pode executar esse trecho de código.
  • Após a conclusão da execução, ele libera automaticamente o bloqueio. Se outro encadeamento precisar operar com o código sincronizado, ele aguardará que o encadeamento atual que está operando nele solte a trava. Esse processo de aquisição e liberação de bloqueios é tratado internamente pela máquina virtual Java. Um programa não é responsável por adquirir e liberar bloqueios pelo encadeamento. Os segmentos restantes podem, no entanto, executar qualquer outro pedaço de código não sincronizado simultaneamente.

Vamos sincronizar nosso exemplo anterior, sincronizando o código dentro do método run usando o bloco sincronizado na classe "Modify", como abaixo:

Class Modify:
package JavaConcepts;
public class Modify implements Runnable(
private int myVar=0;
public int getMyVar() (
return myVar;
)
public void setMyVar(int myVar) (
this.myVar = myVar;
)
public void increment() (
myVar++;
)
@Override
public void run() (
// TODO Auto-generated method stub
synchronized(this) (
this.increment();
System.out.println("Current thread being executed "
+ Thread.currentThread().getName() + " Current Thread value " + this.getMyVar());
)
)
)

O código para a classe "RaceCondition" permanece o mesmo. Agora, ao executar o código, a saída é a seguinte:

Saída1:

O segmento atual que está sendo executado, o segmento 1 Valor do segmento atual 1

O thread atual sendo executado thread 2 Valor atual do thread 2

O thread atual sendo executado thread 3 Valor atual do thread 3

Saída2:

O segmento atual que está sendo executado, o segmento 1 Valor do segmento atual 1

O segmento atual que está sendo executado, o segmento 3 Valor do segmento atual 2

O thread atual sendo executado thread 2 Valor atual do thread 3

Observe que nosso código está fornecendo a saída esperada. Aqui cada thread está incrementando o valor em 1 para a variável "myVar" (na classe "Modify").

Nota: A sincronização é necessária quando vários encadeamentos estão operando no mesmo objeto. Se vários threads estiverem operando em vários objetos, a sincronização não será necessária.

Por exemplo, vamos modificar o código na classe "RaceCondition" como abaixo e trabalhar com a classe anteriormente não sincronizada "Modify".

package JavaConcepts;
public class RaceCondition (
public static void main(String() args) (
Modify mObj = new Modify();
Modify mObj1 = new Modify();
Modify mObj2 = new Modify();
Thread t1 = new Thread(mObj, "thread 1");
Thread t2 = new Thread(mObj1, "thread 2");
Thread t3 = new Thread(mObj2, "thread 3");
t1.start();
t2.start();
t3.start();
)
)

Resultado:

O segmento atual que está sendo executado, o segmento 1 Valor do segmento atual 1

O segmento atual que está sendo executado, o segmento 2 Valor atual do segmento 1

O segmento atual que está sendo executado, o segmento 3 Valor atual do segmento 1

Tipos de sincronização em Java:

Existem dois tipos de sincronização de encadeamento, sendo um mutuamente exclusivo e a outra comunicação entre encadeamentos.

1.Mutualmente Exclusivo

  • Método sincronizado.
  • Método sincronizado estático
  • Bloco sincronizado.

2.Thread Coordenação (Comunicação entre threads em java)

Mutuamente exclusivos:

  • Nesse caso, os encadeamentos obtêm o bloqueio antes de operar em um objeto, evitando trabalhar com objetos que tiveram seus valores manipulados por outros encadeamentos.
  • Isso pode ser alcançado de três maneiras:

Eu. Método sincronizado: podemos usar a palavra-chave “sincronizada” para um método, tornando-o um método sincronizado. Todo encadeamento que chama o método sincronizado obterá o bloqueio para esse objeto e o liberará assim que sua operação for concluída. No exemplo acima, podemos fazer nosso método "run ()" como sincronizado, usando a palavra-chave "synchronized" após o modificador de acesso.

@Override
public synchronized void run() (
// TODO Auto-generated method stub
this.increment();
System.out.println("Current thread being executed "
+ Thread.currentThread().getName() + " Current Thread value " + this.getMyVar());
)

A saída para este caso será:

O segmento atual que está sendo executado, o segmento 1 Valor do segmento atual 1

O segmento atual que está sendo executado, o segmento 3 Valor do segmento atual 2

O thread atual sendo executado thread 2 Valor atual do thread 3

ii. Método sincronizado estático: para sincronizar métodos estáticos, é necessário adquirir seu bloqueio de nível de classe. Depois que um encadeamento obtém apenas o bloqueio no nível da classe, ele poderá executar um método estático. Enquanto um encadeamento mantém o bloqueio no nível da classe, nenhum outro encadeamento pode executar qualquer outro método estático sincronizado dessa classe. No entanto, os outros encadeamentos podem executar qualquer outro método regular ou método estático regular ou mesmo método sincronizado não estático dessa classe.

Por exemplo, vamos considerar nossa classe "Modify" e fazer alterações convertendo nosso método de "incremento" para um método estático sincronizado. As alterações de código são as seguintes:

package JavaConcepts;
public class Modify implements Runnable(
private static int myVar=0;
public int getMyVar() (
return myVar;
)
public void setMyVar(int myVar) (
this.myVar = myVar;
)
public static synchronized void increment() (
myVar++;
System.out.println("Current thread being executed " + Thread.currentThread().getName() + " Current Thread value " + myVar);
)
@Override
public void run() (
// TODO Auto-generated method stub
increment();
)
)

iii. Bloco sincronizado: uma das principais desvantagens do método sincronizado é que ele aumenta o tempo de espera dos threads, afetando o desempenho do código. Portanto, para poder sincronizar apenas as linhas de código necessárias no lugar de todo o método, é necessário fazer uso de um bloco sincronizado. O uso do bloco sincronizado reduz o tempo de espera dos encadeamentos e melhora o desempenho também. No exemplo anterior, já utilizamos o bloco sincronizado enquanto sincronizamos nosso código pela primeira vez.

Exemplo:
public void run() (
// TODO Auto-generated method stub
synchronized(this) (
this.increment();
System.out.println("Current thread being executed "
+ Thread.currentThread().getName() + " Current Thread value " + this.getMyVar());
)
)

Coordenação de Tópicos:

Para encadeamentos sincronizados, a comunicação entre encadeamentos é uma tarefa importante. Os métodos embutidos que ajudam a alcançar a comunicação entre threads para código sincronizado são:

  • esperar()
  • notify ()
  • notifyAll ()

Nota: Esses métodos pertencem à classe de objeto e não à classe de encadeamento. Para que um thread possa invocar esses métodos em um objeto, ele deve estar mantendo o bloqueio nesse objeto. Além disso, esses métodos fazem com que um thread libere seu bloqueio no objeto no qual está sendo chamado.

wait (): um thread ao invocar o método wait (), libera o bloqueio no objeto e entra no estado de espera. Possui duas sobrecargas de método:

  • público final void wait () lança InterruptedException
  • espera pública final nula (tempo limite longo) lança InterruptedException
  • pública final void wait (tempo limite longo, int nanos) lança InterruptedException

notify (): Um thread envia um sinal para outro thread no estado de espera, usando o método notify (). Ele envia a notificação para apenas um thread, para que ele possa retomar sua execução. O encadeamento que receberá a notificação entre todos os encadeamentos no estado de espera depende da Java Virtual Machine.

  • notificação de anulação final pública ()

notifyAll (): quando um thread chama o método notifyAll (), todos os threads em seu estado de espera são notificados. Esses encadeamentos serão executados um após o outro com base na ordem decidida pela Java Virtual Machine.

  • public final void notifyAll ()

Conclusão

Neste artigo, vimos como o trabalho em um ambiente multithread pode levar à inconsistência dos dados devido a uma condição de corrida. Como a sincronização nos ajuda a superar isso, limitando um único encadeamento para operar em um recurso compartilhado por vez. Além disso, como os threads sincronizados se comunicam.

Artigos recomendados:

Este foi um guia para O que é sincronização em Java ?. Aqui discutimos a introdução, compreensão, necessidade, trabalho e tipos de sincronização com algum código de amostra. Você também pode consultar nossos outros artigos sugeridos para saber mais -

  1. Serialização em Java
  2. O que é genérico em Java?
  3. O que é API em Java?
  4. O que é uma árvore binária em Java?
  5. Exemplos e como os genéricos funcionam em c #