Sincronização de campo não final


90

Um aviso é exibido toda vez que eu sincronizo em um campo de classe não final. Aqui está o código:

public class X  
{  
   private Object o;  

   public void setO(Object o)  
   {  
     this.o = o;  
   }  

   public void x()  
   {  
     synchronized (o) // synchronization on a non-final field  
     {  
     }  
   }  
 } 

então mudei a codificação da seguinte maneira:

 public class X  
 {  

   private final Object o;       
   public X()
   {  
     o = new Object();  
   }  

   public void x()  
   {  
     synchronized (o)
     {  
     }  
   }  
 }  

Não tenho certeza se o código acima é a maneira correta de sincronizar em um campo de classe não final. Como posso sincronizar um campo não final?

Respostas:


126

Em primeiro lugar, eu encorajo você a realmente tentar lidar com problemas de simultaneidade em um nível mais alto de abstração, ou seja, resolvendo-o usando classes de java.util.concurrent como ExecutorServices, Callables, Futures etc.

Dito isso, não há nada de errado em sincronizar em um campo não final em si. Você só precisa ter em mente que, se a referência do objeto mudar, a mesma seção de código pode ser executada em paralelo . Ou seja, se um thread executa o código no bloco sincronizado e alguém chama setO(...), outro thread pode executar o mesmo bloco sincronizado na mesma instância simultaneamente.

Sincronize no objeto ao qual você precisa de acesso exclusivo (ou, melhor ainda, um objeto dedicado a protegê-lo).


1
Estou dizendo que, se você sincronizar em um campo não final, deve estar ciente do fato de que o trecho de código é executado com acesso exclusivo ao objeto oreferido no momento em que o bloco sincronizado foi atingido. Se o objeto que ose refere a alterações, outro thread pode vir e executar o bloco de código sincronizado.
aioobe

42
Não concordo com sua regra prática - prefiro sincronizar em um objeto cujo único propósito é proteger outro estado. Se você nunca faz nada com um objeto além de travá-lo, você sabe com certeza que nenhum outro código pode travá-lo. Se você bloquear um objeto "real" cujos métodos você chamará, esse objeto também poderá se sincronizar, o que torna mais difícil raciocinar sobre o bloqueio.
Jon Skeet

9
Como eu disse em minha resposta, acho que precisaria de uma justificativa muito cuidadosa para mim, por que você iria querer fazer tal coisa. E eu não recomendaria sincronizar this, também - eu recomendaria criar uma variável final na classe apenas para fins de bloqueio , o que impede que qualquer outra pessoa bloqueie no mesmo objeto.
Jon Skeet

1
Esse é outro ponto bom, e eu concordo; bloquear uma variável não final definitivamente precisa de uma justificativa cuidadosa.
aioobe

Não tenho certeza sobre os problemas de visibilidade da memória em torno da alteração de um objeto que é usado para sincronização. Acho que você teria grandes problemas para alterar um objeto e, em seguida, confiar no código para ver essa alteração corretamente, de modo que "a mesma seção do código possa ser executada em paralelo". Não tenho certeza de quais garantias, se houver, são estendidas pelo modelo de memória para a visibilidade da memória de campos que são usados ​​para bloquear, em oposição às variáveis ​​acessadas dentro do bloco de sincronização. Minha regra seria, se você sincronizar em algo, deve ser final.
Mike Q

46

Não é realmente uma boa idéia - porque seus blocos sincronizados não são mais realmente sincronizada de uma forma consistente.

Supondo que os blocos sincronizados garantam que apenas um thread acesse alguns dados compartilhados por vez, considere:

  • O thread 1 entra no bloco sincronizado. Sim - tem acesso exclusivo aos dados compartilhados ...
  • O thread 2 chama setO ()
  • Thread 3 (ou ainda 2 ...) entra no bloco sincronizado. Eek! Ele acha que tem acesso exclusivo aos dados compartilhados, mas o thread 1 ainda está lidando com isso ...

Por que você quer que isso aconteça? Talvez haja algumas situações muito especializadas em que faça sentido ... mas você teria que me apresentar um caso de uso específico (junto com maneiras de mitigar o tipo de cenário que apresentei acima) antes de eu ficar feliz com isto.


2
@aioobe: Mas então o thread 1 ainda pode estar executando algum código que está alterando a lista (e freqüentemente se referindo a o) - e no meio de sua execução comece a mutar uma lista diferente. Como isso seria uma boa ideia? Acho que discordamos fundamentalmente sobre se é ou não uma boa ideia travar os objetos que você toca de outras maneiras. Eu preferiria ser capaz de raciocinar sobre meu código sem nenhum conhecimento do que outro código faz em termos de bloqueio.
Jon Skeet

2
@Felype: Parece que você deveria fazer uma pergunta mais detalhada como uma pergunta separada - mas sim, eu geralmente criaria objetos separados apenas como fechaduras.
Jon Skeet

3
@VitBernatik: Não. Se o thread X começar a modificar a configuração, o thread Y muda o valor da variável que está sendo sincronizada, então o thread Z começa a modificar a configuração, então ambos X e Z modificarão a configuração ao mesmo tempo, o que é ruim .
Jon Skeet

1
Em suma, é mais seguro se sempre declararmos esses objetos de bloqueio como finais, correto?
St.Antario

2
@LinkTheProgrammer: "Um método sincronizado sincroniza cada objeto na instância" - não, não sincroniza. Isso simplesmente não é verdade, e você deve rever sua compreensão da sincronização.
Jon Skeet

12

Concordo com um dos comentários de John: Você deve sempre usar um dummy de bloqueio final ao acessar uma variável não final para evitar inconsistências no caso de alterações de referência da variável. Portanto, em qualquer caso e como primeira regra prática:

Regra # 1: Se um campo não for final, sempre use um modelo de bloqueio final (privado).

Razão # 1: você mantém o bloqueio e altera a referência da variável por conta própria. Outro thread esperando fora do bloqueio sincronizado poderá entrar no bloco protegido.

Razão # 2: você mantém o bloqueio e outro thread altera a referência da variável. O resultado é o mesmo: outro thread pode entrar no bloco protegido.

Mas, ao usar um boneco de bloqueio final, há outro problema : você pode obter dados errados, porque seu objeto não final só será sincronizado com a RAM ao chamar synchronize (objeto). Portanto, como uma segunda regra prática:

Regra # 2: Ao bloquear um objeto não final, você sempre precisa fazer as duas coisas: Usar um manequim de bloqueio final e o bloqueio do objeto não final para o bem da sincronização de RAM. (A única alternativa será declarar todos os campos do objeto como voláteis!)

Esses bloqueios também são chamados de "bloqueios aninhados". Observe que você deve chamá-los sempre na mesma ordem, caso contrário, você obterá um bloqueio morto :

public class X {
    private final LOCK;
    private Object o;

    public void setO(Object o){
        this.o = o;  
    }  

    public void x() {
        synchronized (LOCK) {
        synchronized(o){
            //do something with o...
        }
        }  
    }  
} 

Como você pode ver, escrevo os dois cadeados diretamente na mesma linha, porque eles sempre pertencem um ao outro. Assim, você pode até fazer 10 bloqueios de aninhamento:

synchronized (LOCK1) {
synchronized (LOCK2) {
synchronized (LOCK3) {
synchronized (LOCK4) {
    //entering the locked space
}
}
}
}

Observe que este código não será quebrado se você apenas adquirir um bloqueio interno como synchronized (LOCK3)por outras threads. Mas ele vai quebrar se você chamar em outro tópico algo como este:

synchronized (LOCK4) {
synchronized (LOCK1) {  //dead lock!
synchronized (LOCK3) {
synchronized (LOCK2) {
    //will never enter here...
}
}
}
}

Há apenas uma solução alternativa para esses bloqueios aninhados ao lidar com campos não finais:

Regra # 2 - Alternativa: Declare todos os campos do objeto como voláteis. (Não vou falar aqui sobre as desvantagens de fazer isso, por exemplo, evitar qualquer armazenamento em caches de nível x, mesmo para leituras, também.)

Portanto, aioobe está certa: basta usar java.util.concurrent. Ou comece a entender tudo sobre sincronização e faça você mesmo com bloqueios aninhados. ;)

Para obter mais detalhes por que a sincronização em campos não finais quebras, dê uma olhada em meu caso de teste: https://stackoverflow.com/a/21460055/2012947

E para obter mais detalhes por que você precisa sincronizar tudo devido à RAM e aos caches, dê uma olhada aqui: https://stackoverflow.com/a/21409975/2012947


1
Eu acho que você tem que envolver o setter de ocom um synchronized (LOCK) para estabelecer uma relação "acontece antes" entre a configuração e o objeto de leitura o. Estou discutindo isso em uma pergunta semelhante minha: stackoverflow.com/questions/32852464/…
Petrakeas

Eu uso dataObject para sincronizar o acesso aos membros dataObject. Como isso está errado? Se o dataObject começar a apontar para algum lugar diferente, quero que ele seja sincronizado com os novos dados para evitar que threads simultâneos os modifiquem. Algum problema com isso?
Harmen

2

Na verdade, não estou vendo a resposta correta aqui, ou seja, está tudo bem fazer isso.

Eu nem sei por que é um aviso, não há nada de errado com isso. A JVM garante que você obtenha algum objeto válido de volta (ou nulo) ao ler um valor, e você pode sincronizar em qualquer objeto.

Se você planeja realmente alterar o bloqueio enquanto ele está em uso (ao invés de, por exemplo, alterá-lo de um método init, antes de começar a usá-lo), você deve criar a variável que planeja alterar volatile. Então, tudo o que você precisa fazer é sincronizar tanto no objeto antigo quanto no novo, e você pode alterar o valor com segurança

public volatile Object lock;

...

synchronized (lock) {
    synchronized (newObject) {
        lock = newObject;
    }
}

Lá. Não é complicado, escrever código com bloqueios (mutexes) é realmente muito fácil. Escrever código sem eles (código de bloqueio gratuito) é o que é difícil.


Isso pode não funcionar. Diga o começou como ref para O1, então thread T1 bloqueia o (= O1) e O2 e define o para O2. Ao mesmo tempo, o Thread T2 bloqueia O1 e espera T1 para desbloqueá-lo. Ao receber o bloqueio O1, ele será definido como O3. Neste cenário, entre T1 liberando O1 e T2 bloqueando O1, O1 tornou-se inválido para bloqueio através de o. Neste momento, outro thread pode usar o (= O2) para travar e prosseguir sem interrupções na corrida com T2.
GPS

2

EDITAR: Portanto, esta solução (como sugerido por Jon Skeet) pode ter um problema com a atomicidade da implementação de "synchronized (object) {}" enquanto a referência do objeto está mudando. Eu perguntei separadamente e de acordo com o Sr. Erickson não é thread-safe - veja: A entrada em bloco sincronizado é atômica? . Portanto, tome como exemplo de como NÃO fazê-lo - com links por quê;)

Veja o código como funcionaria se synchronized () fosse atômico:

public class Main {
    static class Config{
        char a='0';
        char b='0';
        public void log(){
            synchronized(this){
                System.out.println(""+a+","+b);
            }
        }
    }

    static Config cfg = new Config();

    static class Doer extends Thread {
        char id;

        Doer(char id) {
            this.id = id;
        }

        public void mySleep(long ms){
            try{Thread.sleep(ms);}catch(Exception ex){ex.printStackTrace();}
        }

        public void run() {
            System.out.println("Doer "+id+" beg");
            if(id == 'X'){
                synchronized (cfg){
                    cfg.a=id;
                    mySleep(1000);
                    // do not forget to put synchronize(cfg) over setting new cfg - otherwise following will happend
                    // here it would be modifying different cfg (cos Y will change it).
                    // Another problem would be that new cfg would be in parallel modified by Z cos synchronized is applied on new object
                    cfg.b=id;
                }
            }
            if(id == 'Y'){
                mySleep(333);
                synchronized(cfg) // comment this and you will see inconsistency in log - if you keep it I think all is ok
                {
                    cfg = new Config();  // introduce new configuration
                    // be aware - don't expect here to be synchronized on new cfg!
                    // Z might already get a lock
                }
            }
            if(id == 'Z'){
                mySleep(666);
                synchronized (cfg){
                    cfg.a=id;
                    mySleep(100);
                    cfg.b=id;
                }
            }
            System.out.println("Doer "+id+" end");
            cfg.log();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Doer X = new Doer('X');
        Doer Y = new Doer('Y');
        Doer Z = new Doer('Z');
        X.start();
        Y.start();
        Z.start();
    }

}

1
Isso pode estar bem - mas não sei se há alguma garantia no modelo de memória de que o valor que você sincroniza é o mais recente - não acho que haja qualquer garantia de atomicamente "ler e sincronizar". Pessoalmente, tento evitar a sincronização em monitores que tenham outros usos de qualquer maneira, para simplificar. (Por ter um campo separado, o código torna-se claramente correto em vez de ter que raciocinar sobre isso com cuidado.)
Jon Skeet

@Jon. Obrigado pela resposta! Eu ouço sua preocupação. Concordo, neste caso, o bloqueio externo evitaria a questão da "atomicidade sincronizada". Portanto, seria preferível. Embora possa haver casos em que você deseja introduzir mais configurações no tempo de execução e compartilhar configurações diferentes para grupos de threads diferentes (embora não seja o meu caso). E então essa solução pode se tornar interessante. Eu postei a pergunta stackoverflow.com/questions/29217266/… de atomicidade synchronized () - então veremos se pode ser usado (e alguém responde)
Vit Bernatik

2

AtomicReference se adapta às suas necessidades.

Da documentação java sobre o pacote atômico :

Um pequeno kit de ferramentas de classes que oferece suporte à programação de thread-safe sem bloqueio em variáveis ​​únicas. Em essência, as classes neste pacote estendem a noção de valores voláteis, campos e elementos de matriz para aqueles que também fornecem uma operação de atualização condicional atômica da forma:

boolean compareAndSet(expectedValue, updateValue);

Código de amostra:

String initialReference = "value 1";

AtomicReference<String> someRef =
    new AtomicReference<String>(initialReference);

String newReference = "value 2";
boolean exchanged = someRef.compareAndSet(initialReference, newReference);
System.out.println("exchanged: " + exchanged);

No exemplo acima, você substitui Stringpelo seu próprioObject

Pergunta relacionada de SE:

Quando usar AtomicReference em Java?


1

Se onunca mudar durante o tempo de vida de uma instância de X, a segunda versão é o melhor estilo, independentemente de a sincronização estar envolvida.

Agora, se há algo errado com a primeira versão, é impossível responder sem saber o que mais está acontecendo naquela aula. Eu tenderia a concordar com o compilador que ele parece sujeito a erros (não vou repetir o que os outros disseram).


1

Apenas adicionando meus dois centavos: recebi este aviso quando usei um componente que é instanciado pelo designer, então seu campo não pode ser realmente final, porque o construtor não pode usar parâmetros. Em outras palavras, eu tinha um campo quase final sem a palavra-chave final.

Acho que é por isso que é apenas um aviso: provavelmente você está fazendo algo errado, mas pode estar certo também.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.