Oi.
Imagine que você tem que fazer a seguinte operação:
public class ContaCorrente {
public void saque(long valor) {
int saldo = getSaldo(); //1
saldo = saldo - valor; //2
setSaldo(saldo); //3
}
}
O programa parece, ok, não? Agora, imagine que duas Threads diferentes usem a mesma conta ao mesmo tempo, para fazer dois saques. Na nossa suposição a conta tem R$100,00 e o valor a sacar é R$10,00. Vamos ver a besteira que pode acontecer:
A primeira Thread executa 1; int saldo = getSaldo(); //Saldo = R$100
A segunda Thread executa 1; int saldo = getSaldo(); //Saldo = R$100
A primeira Thread executa 2; saldo = saldo - valor; //Saldo = R$90,00
A primeira Thread executa 3; setSaldo(saldo); //O novo saldo é R$90,00
A segunda Thread executa 2; saldo = saldo - valor //Saldo = R$90,00
A segunda Thread executa 3; setSaldo(saldo); //O saldo final é R$90,00!!!
Ocorreram 2 saques, mas o seu programa aparentemente computou um... o gerente do banco não vai ficar nada feliz com o time do desenvolvimento... nada feliz mesmo.
Qual o problema aqui? Não podemos garantir em que ordem o processador vai executar as Threads. A situação acima poderia ser uma delas. Da mesma fora, a Thread 1 poderia executar integralmente, para só daí a Thread 2 executar e o programa pareceria ter funcionado.
Como resolvemos esse problema? Temos que garantir que a operação de saque seja atômica, ou seja, que seja impossível que duas Threads acessem ela ao mesmo tempo. Para isso, temos a palavra chave synchronized.
A sintaxe do synchronized é:
synchronized (objetoChave) {
// Código sincronizado
}
Imagine o código sincronizado como uma porta desses banheiros de avião. Somente uma pessoa pode ficar lá dentro. Quando uma entrou, ela vira a chave (objetoChave) e todas as outras vêm a luz de "ocupado" do lado de fora e são obrigadas a esperar.
O código da conta corrigido ficaria assim:
public class ContaCorrente {
int[] chave = new int[0]; //Criamos um objeto qualquer para servir de chave
public void saque(long valor) {
synchronized (chave) {
int saldo = getSaldo(); //1
saldo = saldo - valor; //2
setSaldo(saldo); //3
}
}
}
Agora o que acontece? Se ocorre-se a mesma sequencia anterior teríamos:
A primeira Thread pega a chave e executa 1; int saldo = getSaldo(); //Saldo = R$100
A segunda Thread tenta pegar a chave, mas não consegue. Passa então esperar a chave liberar;
A primeira Thread executa 2; saldo = saldo - valor; //Saldo = R$90,00
A primeira Thread executa 3; setSaldo(saldo); //O novo saldo é R$90,00
A segunda Thread pega a chave e executa 1; int saldo = getSaldo(); //Saldo = R$90
A segunda Thread executa 2; saldo = saldo - valor //Saldo = R$80,00
A segunda Thread executa 3; setSaldo(saldo); //O saldo final é R$80,00.
Agora sim! Funcionou! Poderíamos usar essa chave para mais de um método. Por exemplo, o método deposito(int valor) também teria de ser sincronizado.
Mas... você pode estar se perguntando: e quando eu digo que um método é sincronizado? O que acontece?
O java usa o próprio objeto (this) como chave. Ou seja fazer:
public class ContaCorrente {
public synchronized void saque(long valor) {
int saldo = getSaldo(); //1
saldo = saldo - valor; //2
setSaldo(saldo); //3
}
}
É a mesma coisa do que fazer:
public class ContaCorrente {
public void saque(long valor) {
synchronized (this) {
int saldo = getSaldo(); //1
saldo = saldo - valor; //2
setSaldo(saldo); //3
}
}
}
É bom lembrar que a sincronização exige um custo. A chamada a um método sincronizado é centenas de vezes mais lenta do que a chamada a um método não sincronizado. Portanto, você deve usar sincronização apenas quando for necessário. Daí o conselho do Java quanto a classes como StringBuilder e StringBuffer, ArrayList e Vector, etc...
Outra coisa, a sincronização é um dos mecanismos, mas ela sozinha nem sempre garante que uma classe seja Thread Safe.