Multithreading com Java - Métodos Sincronizados
Para um melhor entendimento vamos iniciar esse post com um problema recorrente ao utilizarmos Threads.
Problema
Quando mais de uma Thread faz a leitura ou escrita em um mesmo objeto, podem ocorrer inconsistência no valor desse objeto na memória, como podemos ver no código a seguir.
public class ExampleWithError1 {
private int count = 0;
public static void main(String[] args) {
new ExampleWithError1().run();
}
private void run() {
new Thread(() -> {
for (int i = 0; i < 1_000_000; i++) {
count++;
}
}).start();
new Thread(() -> {
for (int i = 0; i < 1_000_000; i++) {
count++;
}
}).start();
System.out.println(count);
}
}
No código acima, criamos uma variável global chamada
, essa variável é incrementada nos loops executados em 2 Threads, como cada loop é percorrido 1 milhão de vezes, ao executar nosso código a saída esperada é de 2 milhões, mas ao executarmos o código obtemos o seguinte resultado.count
29430
Isso ocorre pois iniciamos as Threads mas não esperamos a conclusão das mesmas para exibirmos o valor de
no console.count
Então vamos fazer alguns ajustes no nosso código, para que só exiba o valor no console quando a execução das Threads forem concluídas.
public class ExampleWithError2 {
private int count = 0;
public static void main(String[] args) throws InterruptedException {
new ExampleWithError2().run();
}
private void run() throws InterruptedException {
var thread1 = new Thread(() -> {
for (int i = 0; i < 1_000_000; i++) {
count++;
}
});
var thread2 = new Thread(() -> {
for (int i = 0; i < 1_000_000; i++) {
count++;
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count);
}
}
Ao executarmos o código acima o resultado obtido ainda não é o esperado.
1087733
O problema agora se encontra no incremento do objeto count, as duas threads fazem um loop que incrementam a variável
, mas como o código esta sendo executado em paralelo, digamos que a primeira Thread tenha percorrido o loop 10 vezes então o valor de count é 10, e assim que a segunda thread começa a percorrer o loop, ela obtém o valor de count que no momento seria 10, mas antes mesmo de incrementar o valor de count para 11 a primeira thread percorreu o loop mais algumas vezes, então o valor de count já foi escrito com um valor maior que 11, mas foi substituído para 11, e isso ocorre inúmeras vezes até que os dois loops sejam percorridos.count
Vamos melhorar novamente o nosso código e adicionar um método que faça o incremento de
e também alterar os loops, fazendo com que façam o uso deste novo método.count
public class ExampleWithError3 {
private int count = 0;
private void increment() {
count++;
}
public static void main(String[] args) throws InterruptedException {
new ExampleWithError3().run();
}
private void run() throws InterruptedException {
var thread1 = new Thread(() -> {
for (int i = 0; i < 1_000_000; i++) {
increment();
}
});
var thread2 = new Thread(() -> {
for (int i = 0; i < 1_000_000; i++) {
increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count);
}
}
Mesmo extraindo o incremento de
para um método o resultado obtido não é o esperado.count
1904285
Solução
Como solução devemos utilizar a sincronização de métodos.
Métodos sincronizados permitem uma estratégia simples para evitar a interferência de threads em erros de consistência de memória. Se um objeto for visível para mais de um thread, todas as leituras ou gravações nas variáveis desse objeto são feitas por meio de métodos sincronizados.
Para tornar um método sincronizado, basta adicionar a palavra-chave
à sua declaração:synchronized
private synchronized void increment() {
count++;
}
Voltando ao nosso código, vamos adicionar a palavra chave de sincronização ao método increment.
public class SuccessfulExample {
private int count = 0;
private synchronized void increment() {
count++;
}
public static void main(String[] args) throws InterruptedException {
new SuccessfulExample().run();
}
private void run() throws InterruptedException {
var thread1 = new Thread(() -> {
for (int i = 0; i < 1_000_000; i++) {
increment();
}
});
var thread2 = new Thread(() -> {
for (int i = 0; i < 1_000_000; i++) {
increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count);
}
}
Não é possível intercalar duas invocações de métodos sincronizados no mesmo objeto. Quando uma thread está executando um método sincronizado para um objeto, todas as outras threads que invocam métodos sincronizados para o mesmo objeto bloqueiam (suspendem a execução) até que a primeira thread seja concluída com o objeto.
Com isso ao executarmos o código acima, conseguimos obter o resultado esperado.
2000000
Exceção
Variáveis finais, que não podem ser modificadas após a construção do objeto, podem ser lidos com segurança através de métodos não sincronizados.
O código fonte dessa aplicação esta no repositório hospedado no GitHub.