multithreading - Diferença entre volátil e sincronizado em Java



java-me synchronized volatile (5)

Eu estou querendo saber a diferença entre declarar uma variável como volatile e sempre acessando a variável em um bloco synchronized(this) em Java?

De acordo com este artigo http://www.javamex.com/tutorials/synchronization_volatile.shtml há muito a ser dito e existem muitas diferenças, mas também algumas semelhanças.

Estou particularmente interessado neste pedaço de informação:

...

  • o acesso a uma variável volátil nunca tem o potencial de bloquear: estamos apenas fazendo uma leitura ou gravação simples, assim, ao contrário de um bloco sincronizado, nunca iremos nos prender a nenhum bloqueio;
  • Como acessar uma variável volátil nunca contém um bloqueio, ela não é adequada para casos em que queremos ler-atualizar-gravar como uma operação atômica (a menos que estejamos preparados para "perder uma atualização");

O que eles querem dizer com read-update-write ? Não é uma gravação também uma atualização ou eles simplesmente significam que a atualização é uma gravação que depende da leitura?

Acima de tudo, quando é mais adequado declarar variáveis volatile vez de acessá-las através de um bloco synchronized ? É uma boa ideia usar o volatile para variáveis ​​que dependem de entrada? Por exemplo, existe uma variável chamada render que é lida através do loop de renderização e definida por um evento keypress?


Answers

tl; dr :

Existem 3 principais problemas com multithreading:

1) Condições da Corrida

2) Cache / memória obsoleta

3) Complier e otimizações de CPU

volatile pode resolver 2 e 3, mas não pode resolver 1. Bloqueios synchronized / explícitos podem resolver 1, 2 e 3.

Elaboração :

1) Considere este código inseguro de thread:

x++;

Embora pareça uma operação, na verdade é 3: ler o valor atual de x da memória, adicionar 1 a ele e salvá-lo de volta na memória. Se alguns threads tentarem fazer isso ao mesmo tempo, o resultado da operação é indefinido. Se x originalmente era 1, depois de 2 threads operando o código, ele pode ser 2 e pode ser 3, dependendo de qual encadeamento concluiu qual parte da operação antes de o controle ser transferido para o outro encadeamento. Esta é uma forma de condição de corrida .

O uso synchronized em um bloco de código o torna atômico - o que significa que as três operações acontecem de uma só vez, e não há como outro segmento entrar no meio e interferir. Portanto, se x for 1 e 2 threads tentarem pré-forma x++ , sabemos que no final será igual a 3. Por isso, resolve o problema da condição de corrida.

synchronized (this) {
   x++; // no problem now
}

Marcando x como volatile não faz x++; atômica, por isso não resolve este problema.

2) Além disso, os threads têm seu próprio contexto - isto é, eles podem armazenar em cache os valores da memória principal. Isso significa que alguns encadeamentos podem ter cópias de uma variável, mas operam em sua cópia de trabalho sem compartilhar o novo estado da variável entre outros encadeamentos.

Considere isso em um segmento, x = 10; . E um pouco mais tarde, em outro segmento, x = 20; . A alteração no valor de x pode não aparecer no primeiro encadeamento, porque o outro encadeamento salvou o novo valor em sua memória de trabalho, mas não o copiou para a memória principal. Ou que copiou para a memória principal, mas o primeiro segmento não atualizou sua cópia de trabalho. Então, se agora o primeiro thread verifica if (x == 20) a resposta será false .

Marcando uma variável como volatile basicamente, diz a todos os threads para ler e escrever operações apenas na memória principal. synchronized informa a cada thread para ir atualizar seu valor da memória principal quando eles entram no bloco e liberar o resultado de volta para a memória principal quando eles saem do bloco.

Note que diferentemente das corridas de dados, a memória obsoleta não é tão fácil de (re) produzir, já que os flushes para a memória principal ocorrem de qualquer maneira.

3) O complier e a CPU podem (sem qualquer forma de sincronização entre threads) tratar todo o código como único thread. O que significa que pode olhar para algum código, que é muito significativo em um aspecto multithreading, e tratá-lo como se fosse single threaded, onde não é tão significativo. Assim, ele pode olhar para um código e decidir, em termos de otimização, reorganizá-lo ou mesmo remover partes dele completamente, se não souber que esse código foi projetado para funcionar em vários segmentos.

Considere o seguinte código:

boolean b = false;
int x = 10;

void threadA() {
    x = 20;
    b = true;
}

void threadB() {
    if (b) {
        System.out.println(x);
    }
}

Você poderia pensar que threadB só poderia imprimir 20 (ou não imprimir qualquer coisa se threadB if-check for executado antes de definir b como true), como b é definido como true somente após x ser definido como 20, mas o compilador / CPU pode decidir reordenar threadA, nesse caso, threadB também pode imprimir 10. Marcando b como volatile garante que ele não será reordenado (ou descartado em certos casos). O que significa threadB só pode imprimir 20 (ou nada). Marcando os métodos como syncrhonized alcançará o mesmo resultado. Também marcar uma variável como volatile só garante que ela não seja reordenada, mas tudo antes / depois dela ainda pode ser reordenado, portanto, a sincronização pode ser mais adequada em alguns cenários.

Observe que, antes do Java 5 New Memory Model, o volatile não resolveu esse problema.


synchronized é o modificador de restrição de acesso de nível de método / nível de bloco. Ele irá certificar-se de que um thread possui o bloqueio para a seção crítica. Apenas o encadeamento, que possui um bloqueio, pode entrar no bloco synchronized . Se outros segmentos estiverem tentando acessar essa seção crítica, eles terão que esperar até que o proprietário atual libere o bloqueio.

volatile é um modificador de acesso variável que força todos os threads a obter o último valor da variável da memória principal. Nenhum bloqueio é necessário para acessar variáveis volatile . Todos os threads podem acessar o valor da variável volátil ao mesmo tempo.

Um bom exemplo para usar variável volátil: variável de Date .

Suponha que você tenha tornado a variável de data volatile . Todos os encadeamentos, que acessam essa variável, sempre obtêm os dados mais recentes da memória principal, de modo que todos os encadeamentos mostrem o valor Real (real) da Data. Você não precisa de threads diferentes mostrando tempo diferente para a mesma variável. Todos os tópicos devem mostrar o valor correto da data.

Dê uma olhada neste article para entender melhor o conceito volatile .

Lawrence Dol cleary explicou sua read-write-update query .

Quanto às suas outras dúvidas

Quando é mais adequado declarar variáveis ​​voláteis do que acessá-las através de sincronização?

Você tem que usar o volatile se você acha que todos os threads devem obter o valor real da variável em tempo real, como o exemplo que expliquei para a variável Date.

É uma boa ideia usar o volátil para variáveis ​​que dependem de entrada?

A resposta será a mesma da primeira consulta.

Consulte este article para melhor compreensão.


Eu gosto jenkov's explicação jenkov's

Visibilidade de objetos compartilhados

Se dois ou mais segmentos estão compartilhando um objeto, sem o uso adequado de declarações voláteis ou sincronização , atualizações para o objeto compartilhado feitas por um segmento podem não estar visíveis para outros segmentos.

Imagine que o objeto compartilhado seja inicialmente armazenado na memória principal. Um encadeamento em execução na CPU um lê o objeto compartilhado em seu cache de CPU. Lá, faz uma alteração no objeto compartilhado. Contanto que o cache da CPU não tenha sido liberado de volta para a memória principal, a versão alterada do objeto compartilhado não estará visível para os threads em execução em outras CPUs. Dessa forma, cada thread pode acabar com sua própria cópia do objeto compartilhado, cada cópia fica em um cache de CPU diferente.

O diagrama a seguir ilustra a situação do esboço. Um encadeamento em execução na CPU esquerda copia o objeto compartilhado em seu cache de CPU e altera sua variável de contagem para 2. Essa alteração não é visível para outros encadeamentos em execução na CPU correta, porque a atualização para contagem não foi liberada de volta para a principal memória ainda.

Para resolver esse problema, você pode usar a palavra-chave volátil do Java . A palavra-chave volátil pode garantir que uma determinada variável seja lida diretamente da memória principal e sempre gravada de volta na memória principal quando atualizada.

Condições da corrida

Se dois ou mais segmentos compartilham um objeto e mais de um segmento atualiza variáveis ​​nesse objeto compartilhado, condições de corrida podem ocorrer.

Imagine se o thread A ler a contagem de variáveis ​​de um objeto compartilhado em seu cache de CPU. Imagine também que o segmento B faz o mesmo, mas em um cache de CPU diferente. Agora o thread A adiciona um para contar e o thread B faz o mesmo. Agora var1 foi incrementado duas vezes, uma vez em cada cache da CPU.

Se esses incrementos tivessem sido realizados sequencialmente, a contagem de variáveis ​​seria incrementada duas vezes e o valor original + 2 seria gravado de volta na memória principal.

No entanto, os dois incrementos foram realizados simultaneamente sem a devida sincronização. Independentemente de qual dos segmentos A e B que gravam sua versão atualizada da contagem de volta à memória principal, o valor atualizado será apenas 1 maior que o valor original, apesar dos dois incrementos.

Este diagrama ilustra uma ocorrência do problema com condições de corrida, conforme descrito acima:

Para resolver esse problema, você pode usar um bloco sincronizado Java . Um bloco sincronizado garante que apenas um segmento possa entrar em uma determinada seção crítica do código a qualquer momento. Blocos sincronizados também garantem que todas as variáveis ​​acessadas dentro do bloco sincronizado serão lidas da memória principal, e quando o encadeamento sair do bloco sincronizado, todas as variáveis ​​atualizadas serão retornadas para a memória principal novamente, independentemente de a variável ser declarada volátil ou não.


É importante entender que há dois aspectos para a segurança de threads.

  1. controle de execução e
  2. visibilidade da memória

O primeiro tem a ver com controlar quando o código é executado (incluindo a ordem na qual as instruções são executadas) e se ele pode ser executado simultaneamente, e o segundo para saber quando os efeitos na memória do que foi feito são visíveis para outros threads. Como cada CPU tem vários níveis de cache entre ela e a memória principal, os threads em execução em CPUs ou núcleos diferentes podem ver "memória" de maneira diferente em qualquer momento, porque os threads podem obter e trabalhar em cópias particulares da memória principal.

O uso synchronized impede que qualquer outro segmento obtenha o monitor (ou bloqueie) para o mesmo objeto , impedindo que todos os blocos de código protegidos pela sincronização no mesmo objeto sejam executados simultaneamente. A sincronização também cria uma barreira de memória "acontece antes", causando uma restrição de visibilidade de memória de tal forma que qualquer coisa feita até o momento em que um thread libera um bloqueio aparece para outro thread adquirindo o mesmo bloqueio antes de adquirir o bloqueio. Em termos práticos, no hardware atual, isso normalmente faz com que o cache da CPU seja liberado quando um monitor é adquirido e grava na memória principal quando é liberado, sendo ambos (relativamente) caros.

O uso de volatile , por outro lado, força todos os acessos (ler ou gravar) para a variável volátil a ocorrer na memória principal, efetivamente mantendo a variável volátil fora dos caches da CPU. Isso pode ser útil para algumas ações em que é simplesmente necessário que a visibilidade da variável esteja correta e que a ordem dos acessos não seja importante. O uso de volatile também altera o tratamento de long e double para exigir que os acessos sejam atômicos; em alguns hardwares (mais antigos) isso pode exigir bloqueios, embora não no hardware moderno de 64 bits. Sob o novo modelo de memória (JSR-133) para Java 5+, a semântica dos voláteis foi reforçada para ser quase tão forte quanto sincronizada em relação à visibilidade de memória e ordenação de instruções (veja http://www.cs.umd.edu/users/pugh/java/memoryModel/jsr-133-faq.html#volatile ). Para fins de visibilidade, cada acesso a um campo volátil age como meia sincronização.

Sob o novo modelo de memória, ainda é verdade que as variáveis ​​voláteis não podem ser reordenadas entre si. A diferença é que agora não é mais tão fácil reordenar os acessos normais de campo em torno deles. Escrever em um campo volátil tem o mesmo efeito de memória que o lançamento de um monitor, e a leitura de um campo volátil tem o mesmo efeito de memória que um monitor adquire. Com efeito, como o novo modelo de memória impõe restrições mais rigorosas à reordenação de acessos de campo voláteis com outros acessos de campo, volátil ou não, qualquer coisa visível para o thread A quando ele grava no campo volátil se torna visível para o thread B quando ele lê f .

- FAQ JSR 133 (Java Memory Model)

Portanto, agora, as duas formas de barreira de memória (sob o JMM atual) causam uma barreira de reordenamento de instruções que impede que o compilador ou o tempo de execução reordene as instruções na barreira. No antigo JMM, o volátil não impedia a reordenação. Isso pode ser importante, porque além das barreiras de memória, a única limitação imposta é que, para qualquer segmento em particular , o efeito líquido do código é o mesmo que seria se as instruções fossem executadas precisamente na ordem em que aparecem no arquivo. fonte.

Um uso de volátil é que um objeto compartilhado, mas imutável, é recriado em tempo real, com muitos outros segmentos tomando uma referência ao objeto em um determinado ponto em seu ciclo de execução. É necessário que os outros encadeamentos comecem a usar o objeto recriado depois de publicado, mas não precisa da sobrecarga adicional de sincronização completa e sua contenção de atendente e liberação de cache.

// Declaration
public class SharedLocation {
    static public SomeObject someObject=new SomeObject(); // default object
    }

// Publishing code
// Note: do not simply use SharedLocation.someObject.xxx(), since although
//       someObject will be internally consistent for xxx(), a subsequent 
//       call to yyy() might be inconsistent with xxx() if the object was 
//       replaced in between calls.
SharedLocation.someObject=new SomeObject(...); // new object is published

// Using code
private String getError() {
    SomeObject myCopy=SharedLocation.someObject; // gets current copy
    ...
    int cod=myCopy.getErrorCode();
    String txt=myCopy.getErrorText();
    return (cod+" - "+txt);
    }
// And so on, with myCopy always in a consistent state within and across calls
// Eventually we will return to the code that gets the current SomeObject.

Falando com sua pergunta read-update-write, especificamente. Considere o seguinte código não seguro:

public void updateCounter() {
    if(counter==1000) { counter=0; }
    else              { counter++; }
    }

Agora, com o método updateCounter () não sincronizado, dois segmentos podem inseri-lo ao mesmo tempo. Entre as muitas permutações do que poderia acontecer, uma é que o thread-1 faz o teste para o contador == 1000 e o considera verdadeiro e é então suspenso. Em seguida, o thread-2 faz o mesmo teste e também o vê como verdadeiro e está suspenso. Em seguida, o thread-1 é retomado e define o contador como 0. Em seguida, o thread-2 é retomado e novamente define o contador como 0 porque perdeu a atualização do thread-1. Isso também pode acontecer mesmo se a troca de encadeamento não ocorrer como descrevi, mas simplesmente porque duas cópias de contador em cache diferentes estavam presentes em dois núcleos de CPU diferentes e cada encadeamento foi executado em um núcleo separado. Aliás, um thread poderia ter um contador em um valor e o outro poderia ter um valor totalmente diferente por causa do cache.

O que é importante neste exemplo é que o contador de variáveis ​​foi lido da memória principal no cache, atualizado em cache e somente gravado de volta na memória principal em algum ponto indeterminado depois quando ocorreu uma barreira de memória ou quando a memória cache foi necessária para outra coisa. Tornar o contador volatile é insuficiente para segurança de thread deste código, porque o teste para o máximo e as atribuições são operações discretas, incluindo o incremento que é um conjunto de instruções não-atômicas read+increment+write machine, algo como:

MOV EAX,counter
INC EAX
MOV counter,EAX

As variáveis ​​voláteis são úteis somente quando todas as operações executadas nelas são "atômicas", como meu exemplo em que uma referência a um objeto totalmente formado é somente lida ou escrita (e, na verdade, normalmente é escrita apenas de um único ponto). Outro exemplo seria uma referência de matriz volátil que faz o backup de uma lista de cópia na gravação, desde que a matriz tenha sido lida apenas obtendo primeiro uma cópia local da referência a ela.


Deixe-me tentar explicar minha compreensão com a ajuda de quatro exemplos. Java é passagem por valor e não passagem por referência

/ **

Passe por Valor

Em Java, todos os parâmetros são passados ​​por valor, ou seja, a atribuição de um argumento de método não é visível para o chamador.

* /

Exemplo 1:

public class PassByValueString {
    public static void main(String[] args) {
        new PassByValueString().caller();
    }

    public void caller() {
        String value = "Nikhil";
        boolean valueflag = false;
        String output = method(value, valueflag);
        /*
         * 'output' is insignificant in this example. we are more interested in
         * 'value' and 'valueflag'
         */
        System.out.println("output : " + output);
        System.out.println("value : " + value);
        System.out.println("valueflag : " + valueflag);

    }

    public String method(String value, boolean valueflag) {
        value = "Anand";
        valueflag = true;
        return "output";
    }
}

Resultado

output : output
value : Nikhil
valueflag : false

Exemplo 2:

/ ** * * passar por valor * * /

public class PassByValueNewString {
    public static void main(String[] args) {
        new PassByValueNewString().caller();
    }

    public void caller() {
        String value = new String("Nikhil");
        boolean valueflag = false;
        String output = method(value, valueflag);
        /*
         * 'output' is insignificant in this example. we are more interested in
         * 'value' and 'valueflag'
         */
        System.out.println("output : " + output);
        System.out.println("value : " + value);
        System.out.println("valueflag : " + valueflag);

    }

    public String method(String value, boolean valueflag) {
        value = "Anand";
        valueflag = true;
        return "output";
    }
}

Resultado

output : output
value : Nikhil
valueflag : false

Exemplo 3:

/ ** Este 'Pass By Value tem a sensação de' Pass By Reference '

Algumas pessoas dizem que os tipos primitivos e 'String' são 'pass by value' e os objetos são 'pass by reference'.

Mas, a partir desse exemplo, podemos entender que é apenas passagem por valor, tendo em mente que aqui estamos passando a referência como o valor. ie: referência é passada por valor. É por isso que são capazes de mudar e ainda é verdade depois do escopo local. Mas não podemos alterar a referência real fora do escopo original. o que isso significa é demonstrado pelo próximo exemplo de PassByValueObjectCase2.

* /

public class PassByValueObjectCase1 {

    private class Student {
        int id;
        String name;
        public Student() {
        }
        public Student(int id, String name) {
            super();
            this.id = id;
            this.name = name;
        }
        public int getId() {
            return id;
        }
        public void setId(int id) {
            this.id = id;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        @Override
        public String toString() {
            return "Student [id=" + id + ", name=" + name + "]";
        }
    }

    public static void main(String[] args) {
        new PassByValueObjectCase1().caller();
    }

    public void caller() {
        Student student = new Student(10, "Nikhil");
        String output = method(student);
        /*
         * 'output' is insignificant in this example. we are more interested in
         * 'student'
         */
        System.out.println("output : " + output);
        System.out.println("student : " + student);
    }

    public String method(Student student) {
        student.setName("Anand");
        return "output";
    }
}

Resultado

output : output
student : Student [id=10, name=Anand]

Exemplo 4:

/ **

Além do que foi mencionado no Example3 (PassByValueObjectCase1.java), não podemos alterar a referência real fora do escopo original. "

Nota: Eu não estou colando o código para private class Student. A definição de classe Studenté igual a Example3.

* /

public class PassByValueObjectCase2 {

    public static void main(String[] args) {
        new PassByValueObjectCase2().caller();
    }

    public void caller() {
        // student has the actual reference to a Student object created
        // can we change this actual reference outside the local scope? Let's see
        Student student = new Student(10, "Nikhil");
        String output = method(student);
        /*
         * 'output' is insignificant in this example. we are more interested in
         * 'student'
         */
        System.out.println("output : " + output);
        System.out.println("student : " + student); // Will it print Nikhil or Anand?
    }

    public String method(Student student) {
        student = new Student(20, "Anand");
        return "output";
    }

}

Resultado

output : output
student : Student [id=10, name=Nikhil]




java multithreading java-me synchronized volatile