Você está perguntando especificamente sobre como eles funcionam internamente , então aqui está você:
Sem sincronização
private int counter;
public int getNextUniqueIndex() {
return counter++;
}
Basicamente, lê o valor da memória, incrementa e coloca de volta na memória. Isso funciona em thread único, mas hoje em dia, na era dos caches de vários núcleos, várias CPUs e vários níveis, não funcionará corretamente. Antes de tudo, ele introduz a condição de corrida (vários threads podem ler o valor ao mesmo tempo), mas também problemas de visibilidade. O valor pode ser armazenado apenas na memória da CPU " local " (algum cache) e não estar visível para outras CPUs / núcleos (e, portanto, - threads). É por isso que muitos se referem à cópia local de uma variável em um encadeamento. É muito inseguro. Considere este código popular, mas quebrado, de parada de encadeamento:
private boolean stopped;
public void run() {
while(!stopped) {
//do some work
}
}
public void pleaseStop() {
stopped = true;
}
Adicione volatile
à stopped
variável e ela funcionará bem - se qualquer outro thread modificar a stopped
variável por meio do pleaseStop()
método, você terá certeza de ver essa alteração imediatamente no while(!stopped)
loop do thread de trabalho . BTW, também não é uma boa maneira de interromper um encadeamento, consulte: Como parar um encadeamento em execução para sempre sem qualquer uso e Interrompendo um encadeamento java específico .
AtomicInteger
private AtomicInteger counter = new AtomicInteger();
public int getNextUniqueIndex() {
return counter.getAndIncrement();
}
A AtomicInteger
classe usa operações de CPU de baixo nível CAS ( comparar e trocar ) (não é necessária sincronização!). Elas permitem modificar uma variável específica apenas se o valor presente for igual a outra coisa (e for retornado com êxito). Portanto, quando você o executa, getAndIncrement()
ele roda em loop (implementação real simplificada):
int current;
do {
current = get();
} while(!compareAndSet(current, current + 1));
Então basicamente: leia; tente armazenar valor incrementado; se não for bem-sucedido (o valor não é mais igual a current
), leia e tente novamente. O compareAndSet()
é implementado no código nativo (assembly).
volatile
sem sincronização
private volatile int counter;
public int getNextUniqueIndex() {
return counter++;
}
Este código não está correto. Ele corrige o problema de visibilidade ( volatile
garante que outros threads possam ver as alterações feitas counter
), mas ainda tem uma condição de corrida. Isso foi explicado várias vezes: o pré / pós-incremento não é atômico.
O único efeito colateral de volatile
" liberar " os caches para que todas as outras partes vejam a versão mais recente dos dados. Isso é muito rigoroso na maioria das situações; é por isso que volatile
não é padrão.
volatile
sem sincronização (2)
volatile int i = 0;
void incIBy5() {
i += 5;
}
O mesmo problema acima, mas ainda pior, porque i
não é private
. A condição de corrida ainda está presente. Por que isso é um problema? Se, digamos, dois encadeamentos executam esse código simultaneamente, a saída pode ser + 5
ou + 10
. No entanto, você está garantido para ver a alteração.
Independente múltiplo synchronized
void incIBy5() {
int temp;
synchronized(i) { temp = i }
synchronized(i) { i = temp + 5 }
}
Surpresa, esse código também está incorreto. De fato, está completamente errado. Antes de tudo, você está sincronizando i
, o que está prestes a ser alterado (além disso, i
é um primitivo, então eu acho que você está sincronizando em um temporário Integer
criado via autoboxing ...) Completamente defeituoso. Você também pode escrever:
synchronized(new Object()) {
//thread-safe, SRSLy?
}
Não há dois segmentos que podem entrar no mesmo synchronized
bloco com o mesmo bloqueio . Nesse caso (e da mesma forma em seu código), o objeto de bloqueio é alterado a cada execução e, portanto, synchronized
efetivamente não tem efeito.
Mesmo se você tiver usado uma variável final (ou this
) para sincronização, o código ainda está incorreto. Primeiro, dois threads podem ler i
de forma temp
síncrona (tendo o mesmo valor localmente temp
), depois o primeiro atribui um novo valor a i
(digamos, de 1 a 6) e o outro faz a mesma coisa (de 1 a 6).
A sincronização deve abranger desde a leitura até a atribuição de um valor. Sua primeira sincronização não tem efeito (a leitura de um int
é atômica) e a segunda também. Na minha opinião, estas são as formas corretas:
void synchronized incIBy5() {
i += 5
}
void incIBy5() {
synchronized(this) {
i += 5
}
}
void incIBy5() {
synchronized(this) {
int temp = i;
i = temp + 5;
}
}