Por que esta classe não é thread-safe?

94
class ThreadSafeClass extends Thread
{
     private static int count = 0;

     public synchronized static void increment()
     {
         count++;
     }

     public synchronized void decrement()
     {
         count--;
     }
}

Alguém pode explicar por que a classe acima não é thread-safe?

das kinder
fonte
6
Não sei sobre Java, mas parece que cada um desses métodos é individualmente seguro para thread, mas você poderia ter um thread em cada um dos métodos ao mesmo tempo. Talvez se você tiver um único método que leva um bool ( increment), seria thread-safe. Ou se você usou algum objeto de bloqueio. Como eu disse, não sei sobre Java - meu comentário vem do conhecimento de C #.
Wai Ha Lee
Eu também não conheço bem o Javavery, mas para sincronizar o acesso a uma variável estática, o synchronizeddeve ser usado apenas em métodos estáticos. Portanto, em minha opinião, mesmo que você remova o incrementmétodo, ele ainda não é thread-safe, pois duas instâncias (que têm apenas acesso sincronizado por meio da mesma instância) podem chamar o método simultaneamente.
Onur
4
É thread-safe, desde que você nunca crie uma instância da classe.
Benjamin Gruenbaum
1
Por que você acha que não é thread-safe.
Raedwald

Respostas:

134

Como o incrementmétodo é, staticele sincronizará no objeto de classe para o ThreadSafeClass. O decrementmétodo não é estático e será sincronizado na instância usada para chamá-lo. Ou seja, eles serão sincronizados em objetos diferentes e, portanto, dois threads diferentes podem executar os métodos ao mesmo tempo. Como as operações ++e --não são atômicas, a classe não é segura para threads.

Além disso, uma vez que counté static, modificá-lo a partir de decrementque é um sincronizado exemplo método não é seguro uma vez que pode ser chamado em casos diferentes e modificar countsimultaneamente dessa forma.

K Erlandsson
fonte
12
Você pode adicionar, já que counté static, ter um método de instância decrement()é errado, mesmo se não houvesse static increment()método, como dois segmentos podem invocar decrement()em diferentes instâncias modificando o mesmo contador simultaneamente.
Holger
1
Essa é possivelmente uma boa razão para preferir o uso de synchronizedblocos em geral (mesmo em todo o conteúdo do método) em vez de usar o modificador no método, ou seja, synchronized(this) { ... }e synchronized(ThreadSafeClass.class) { ... }.
Bruno
++e --não são atômicas, mesmo ligadasvolatile int . Synchronizedcuida do problema de leitura / atualização / gravação com ++/ --, mas a staticpalavra-chave é, bem, a chave aqui. Boa resposta!
Chris Cirefice
Re, modificar [um campo estático] de ... um método de instância sincronizada é errado : não há nada inerentemente errado em acessar uma variável estática de dentro de um método de instância, e também não há nada inerentemente errado em acessá-la a partir de um synchronizedmétodo de instância. Só não espere "sincronizado" no método de instância para fornecer proteção para os dados estáticos. O único problema aqui é o que você disse no primeiro parágrafo: os métodos usam bloqueios diferentes na tentativa de proteger os mesmos dados e isso, é claro, não oferece proteção alguma.
Solomon Slow
23

Você tem dois métodos sincronizados, mas um deles é estático e o outro não. Ao acessar um método sincronizado, baseado em seu tipo (estático ou não estático), um objeto diferente será bloqueado. Para um método estático, um bloqueio será colocado no objeto Class, enquanto para o bloco não estático, um bloqueio será colocado na instância da classe que executa o método. Como você tem dois objetos bloqueados diferentes, pode ter dois threads que modificam o mesmo objeto simultaneamente.

Slimu
fonte
14

Alguém pode explicar por que a classe acima não é thread-safe?

  • increment sendo estático, a sincronização será feita na própria classe.
  • decrementsendo não estático, a sincronização será feita na instanciação do objeto, mas isso não protege nada como countestático.

Eu gostaria de adicionar isso para declarar um contador thread-safe, acredito que a maneira mais simples é usar em AtomicIntegervez de um int primitivo.

Deixe-me redirecioná-lo para as java.util.concurrent.atomicinformações do pacote.

Jean-François Savard
fonte
7

As respostas dos outros são muito boas e explicam o motivo. Acabei de adicionar algo para resumir synchronized:

public class A {
    public synchronized void fun1() {}

    public synchronized void fun2() {}

    public void fun3() {}

    public static synchronized void fun4() {}

    public static void fun5() {}
}

A a1 = new A();

synchronizedativado fun1e fun2é sincronizado no nível do objeto de instância. synchronizedon fun4é sincronizado no nível do objeto de classe. Que significa:

  1. Quando 2 threads chamam a1.fun1()ao mesmo tempo, a última chamada será bloqueada.
  2. Quando o thread 1 chama a1.fun1()e o thread 2 chama a1.fun2()ao mesmo tempo, a última chamada será bloqueada.
  3. Quando encadear 1 chamada a1.fun1()e encadear 2 chamadaa1.fun3() ao mesmo tempo, sem bloqueio, os 2 métodos serão executados ao mesmo tempo.
  4. Quando o thread 1 chama A.fun4(), se outros threads chamam A.fun4()ou A.fun5()ao mesmo tempo, as últimas chamadas serão bloqueadas, poissynchronizedfun4 esteja no nível de classe.
  5. Quando o thread 1 chama A.fun4(), o thread 2 chama a1.fun1()ao mesmo tempo, sem bloqueio, os 2 métodos serão executados ao mesmo tempo.
coderz
fonte
6
  1. decrement é travar uma coisa diferente para increment que não impeçam um ao outro de funcionar.
  2. Chamar decrementuma instância é bloquear algo diferente de chamar decrementoutra instância, mas eles estão afetando a mesma coisa.

O primeiro significa que chamadas sobrepostas para incrementedecrement podem resultar em um cancelamento (correto), um incremento ou um decremento.

O segundo significa que duas chamadas sobrepostas para decrementem instâncias diferentes podem resultar em um decréscimo duplo (correto) ou um decremento único.

Jon Hanna
fonte
4

Uma vez que dois métodos diferentes, um é o nível de instância e outro é o nível de classe, então você precisa bloquear 2 objetos diferentes para torná-lo ThreadSafe

jaleel_quest
fonte
1

Conforme explicado em outras respostas, seu código não é seguro para thread, pois o método estático increment()bloqueia o monitor de classe e o método não estáticodecrement() bloqueia o monitor de objeto.

Para este exemplo de código, existe uma solução melhor sem o synchronzeduso de palavras-chave. Você tem que usar AtomicInteger para alcançar a segurança do Thread.

Segure a linha usando AtomicInteger:

import java.util.concurrent.atomic.AtomicInteger;

class ThreadSafeClass extends Thread {

    private static AtomicInteger count = new AtomicInteger(0);

    public static void increment() {
        count.incrementAndGet();
    }

    public static void decrement() {
        count.decrementAndGet();
    }

    public static int value() {
        return count.get();
    }

}
Ravindra babu
fonte