Qual é o problema de concorrência mais frequente que você encontrou em Java? [fechadas]

191

Esta é uma pesquisa de tipos sobre problemas de simultaneidade comuns em Java. Um exemplo pode ser o impasse clássico ou a condição de corrida ou talvez os erros de segmentação EDT no Swing. Estou interessado tanto em uma variedade de possíveis problemas, mas também em quais são os problemas mais comuns. Portanto, deixe uma resposta específica de um bug de concorrência Java por comentário e vote se você encontrar uma que encontrou.

Alex Miller
fonte
16
Por que isso está fechado? Isso é útil para outros programadores que imploram simultaneidade em Java e para ter uma idéia de quais classes de defeitos de simultaneidade estão sendo mais observadas por outros desenvolvedores de Java.
31713
@ Longpoke A mensagem de encerramento explica por que está fechada. Esta não é uma pergunta com uma resposta "correta" específica, é mais uma pergunta de enquete / lista. E o Stack Overflow não pretende hospedar esse tipo de perguntas. Se você não concorda com essa política, convém discuti-la na meta .
Andrzej Doyle 28/05
7
Acho que a comunidade discorda, pois este artigo está recebendo mais de 100 visualizações / dia! Eu achei isso muito útil, pois estou envolvido com o desenvolvimento de uma ferramenta de análise estática projetada especificamente para corrigir problemas de simultaneidade contemplateltd.com/threadsafe . Ter um banco de problemas de simultaneidade comumente encontrados foi ótimo para testar e melhorar o ThreadSafe.
Craig Manson
A lista de verificação de revisão de código para a Concorrência Java digere a maioria das armadilhas mencionadas nas respostas a esta pergunta de forma conveniente para as revisões diárias do código.
leventov 01/09/19

Respostas:

125

O problema de concorrência mais comum que eu já vi, não é perceber que um campo gravado por um thread não tem a garantia de ser visto por outro thread. Uma aplicação comum disso:

class MyThread extends Thread {
  private boolean stop = false;

  public void run() {
    while(!stop) {
      doSomeWork();
    }
  }

  public void setStop() {
    this.stop = true;
  }
}

Enquanto parada não é volátil , ou setStope runnão são sincronizados isso não é garantido que funcione. Esse erro é especialmente diabólico, pois em 99,999% não importa na prática, pois o segmento do leitor verá a mudança - mas não sabemos quanto tempo ele viu.

Kutzi
fonte
9
Uma ótima solução para isso é tornar a variável de instância de parada um AtomicBoolean. Resolve todos os problemas dos não voláteis, enquanto protege você dos problemas do JMM.
22110 Kirk Wylie
39
É pior do que 'por vários minutos' - você NUNCA poderá vê-lo. Sob o Modelo de Memória, a JVM tem permissão para otimizar while (! Stop) em while (true) e, em seguida, você recebe uma mangueira. Isso pode acontecer apenas em algumas VMs, apenas no modo de servidor, somente quando a JVM é recompilada após x iterações do loop, etc.
11289 Cowan
2
Por que você gostaria de usar AtomicBoolean sobre booleano volátil? Estou desenvolvendo para a versão 1.4+, então existem armadilhas ao declarar voláteis?
Piscina
2
Nick, acho que é porque o CAS atômico é geralmente ainda mais rápido que volátil. Se você está desenvolvendo para 1,4 seu IMHO opção só segura é usar sincronizado tão volátil em 1.4 não tem os fortes garantias de barreira de memória, pois tem em Java 5.
Kutzi
5
@ Thomas: isso é por causa do modelo de memória Java. Você deve ler sobre isso, se quiser conhecê-lo em detalhes (o Java Concurrency in Practice de Brian Goetz explica bem, por exemplo). Resumindo: a menos que você use palavras-chave / construções de sincronização de memória (como volátil, sincronizada, AtomicXyz, mas também quando um Thread for concluído), um Thread NÃO tem garantia alguma de ver as alterações feitas em qualquer campo feitas por um thread diferente
Kutzi
178

Meu problema de concorrência mais doloroso nº 1 já ocorreu quando duas bibliotecas de código aberto diferentes fizeram algo parecido com isto:

private static final String LOCK = "LOCK";  // use matching strings 
                                            // in two different libraries

public doSomestuff() {
   synchronized(LOCK) {
       this.work();
   }
}

À primeira vista, isso parece um exemplo de sincronização bastante trivial. Contudo; como Strings são internadas em Java, a string literal "LOCK"acaba sendo a mesma instância de java.lang.String(mesmo que sejam declaradas de maneira completamente díspar uma da outra). O resultado é obviamente ruim.

Jared
fonte
62
Essa é uma das razões pelas quais eu prefiro a estática final privada privada Object LOCK = new Object ();
Andrzej Doyle
17
Eu amo isso - oh, isso é desagradável :) #
Thorbjørn Ravn Andersen
7
Isso é uma boa para Java Puzzlers 2.
Dov Wasserman
12
Na verdade ... isso realmente me faz querer que o compilador se recuse a permitir que você sincronize em uma String. Dada a internação de String, não há caso em que isso seria uma "coisa boa (tm)".
Jared
3
@ Jared: "até que a string esteja internada" não faz sentido. As cordas não "magicamente" ficam internadas. String.intern () retorna um objeto diferente, a menos que você já tenha a instância canônica da String especificada. Além disso, todas as strings literais e expressões constantes com valor de string são internadas. Sempre. Consulte os documentos para String.intern () e §3.10.5 do JLS.
Laurence Gonsalves
65

Um problema clássico é alterar o objeto no qual você está sincronizando enquanto sincroniza:

synchronized(foo) {
  foo = ...
}

Outros threads simultâneos são sincronizados em um objeto diferente e esse bloco não fornece a exclusão mútua que você espera.

Alex Miller
fonte
19
Há uma inspeção da IDEA para isso chamada "Sincronização em campo não final, que provavelmente não terá semântica útil". Muito agradável.
194 Jen S.
8
Ha ... agora essa é uma descrição torturada. "é improvável que tenha semântica útil" poderia ser melhor descrito como "provavelmente quebrado". :)
Alex Miller
Eu acho que foi o Java amargo que teve isso em seu ReadWriteLock. Felizmente agora temos java.util.concurrency.locks e Doug é um pouco mais ativo.
Tom Hawtin - tackline
Eu também já vi esse problema com frequência. Sincronize apenas objetos finais, nesse caso. FindBugs et al. ajuda sim.
gimpf 23/01/09
isso é apenas um problema durante a tarefa? (veja o exemplo de @Alex Miller abaixo com um mapa) Esse exemplo de mapa também teria esse mesmo problema?
Alex Beardsley
50

Um problema comum é usar classes como Calendar e SimpleDateFormat de vários threads (geralmente armazenando-os em cache em uma variável estática) sem sincronização. Essas classes não são seguras para threads, portanto, o acesso multiencadeado acabará causando problemas estranhos com estado inconsistente.

Alex Miller
fonte
Você conhece algum projeto de código aberto que contenha esse bug em alguma versão dele? Estou procurando exemplos concretos desse bug no software do mundo real.
Reprogrammer
47

Bloqueio verificado duas vezes. Em geral.

O paradigma, no qual comecei a aprender os problemas de quando estava trabalhando na BEA, é que as pessoas verifiquem um singleton da seguinte maneira:

public Class MySingleton {
  private static MySingleton s_instance;
  public static MySingleton getInstance() {
    if(s_instance == null) {
      synchronized(MySingleton.class) { s_instance = new MySingleton(); }
    }
    return s_instance;
  }
}

Isso nunca funciona, porque outro encadeamento pode ter entrado no bloco sincronizado e s_instance não é mais nulo. Portanto, a mudança natural é:

  public static MySingleton getInstance() {
    if(s_instance == null) {
      synchronized(MySingleton.class) {
        if(s_instance == null) s_instance = new MySingleton();
      }
    }
    return s_instance;
  }

Isso também não funciona, porque o Java Memory Model não é compatível. Você precisa declarar s_instance como volátil para fazê-lo funcionar e, mesmo assim, ele funciona apenas no Java 5.

Pessoas que não estão familiarizadas com os meandros do Java Memory Model mexem nisso o tempo todo .

Kirk Wylie
fonte
7
O padrão enum singleton resolve todos esses problemas (veja os comentários de Josh Bloch sobre isso). O conhecimento de sua existência deve ser mais difundido entre os programadores Java.
Robin
Eu ainda tenho que encontrar um único caso em que a inicialização lenta de um singleton era realmente apropriada. E se for, apenas declare o método sincronizado.
Dov Wasserman
3
É isso que eu uso para a inicialização Lazy das classes Singleton. Além disso, nenhuma sincronização é necessária, pois isso é garantido pelo java implicitamente. classe Foo {estático da classe Holder {static Foo foo = new Foo (); } static Foo getInstance () {return Holder.foo; }}
Irfan Zulfiqar
Irfan, que é chamado o método de Pugh, pelo que me lembro
Chris R
@ Robin, não é mais simples usar apenas um inicializador estático? É sempre garantido que eles sejam executados sincronizados.
Matt b
47

Não sincronizando corretamente os objetos retornados por Collections.synchronizedXXX(), especialmente durante a iteração ou várias operações:

Map<String, String> map = Collections.synchronizedMap(new HashMap<String, String>());

...

if(!map.containsKey("foo"))
    map.put("foo", "bar");

Isso está errado . Apesar de operações únicas synchronized, o estado do mapa entre a chamada containse putpode ser alterado por outro encadeamento. Deveria ser:

synchronized(map) {
    if(!map.containsKey("foo"))
        map.put("foo", "bar");
}

Ou com uma ConcurrentMapimplementação:

map.putIfAbsent("foo", "bar");
Dave Ray
fonte
5
Ou melhor, use um ConcurrentHashMap e putIfAbsent.
Tom Hawtin - tackline
37

Embora provavelmente não seja exatamente o que você está procurando, o problema mais frequente relacionado à concorrência que encontrei (provavelmente porque aparece no código de thread único normal) é um

java.util.ConcurrentModificationException

causada por coisas como:

List<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c"));
for (String string : list) { list.remove(string); }
Fabian Steeg
fonte
Não, é totalmente isso que estou procurando. Obrigado!
Alex Miller
30

Pode ser fácil pensar que as coleções sincronizadas lhe concedem mais proteção do que realmente oferecem e esqueça de manter o bloqueio entre as chamadas. Eu já vi esse erro algumas vezes:

 List<String> l = Collections.synchronizedList(new ArrayList<String>());
 String[] s = l.toArray(new String[l.size()]);

Por exemplo, na segunda linha acima, os métodos toArray()e size()são ambos thread-safe por si só, mas o size()é avaliado separadamente do toArray()e o bloqueio na lista não é mantido entre essas duas chamadas.

Se você executar esse código com outro thread removendo simultaneamente os itens da lista, mais cedo ou mais tarde você terminará com um novoString[] retorno maior que o necessário para conter todos os elementos da lista e ter valores nulos na cauda. É fácil pensar que, como as duas chamadas de método para a Lista ocorrem em uma única linha de código, é de alguma forma uma operação atômica, mas não é.

Nick
fonte
5
bom exemplo. Eu acho que classificaria isso mais geralmente como "a composição das operações atômicas não é atômica". (Veja campo volátil ++ para um outro exemplo simples)
Alex Miller
29

O erro mais comum que vemos onde trabalho é que os programadores executam operações longas, como chamadas de servidor, no EDT, bloqueando a GUI por alguns segundos e deixando o aplicativo sem resposta.

Eric Burke
fonte
uma dessas respostas Eu gostaria de poder dar mais do que um ponto para
Epaga
2
EDT = thread de
distribuição de
28

Esquecendo de wait () (ou Condition.await ()) em um loop, verificando se a condição de espera é realmente verdadeira. Sem isso, você encontra bugs de ativações espúrias de wait (). O uso canônico deve ser:

 synchronized (obj) {
     while (<condition does not hold>) {
         obj.wait();
     }
     // do stuff based on condition being true
 }
Alex Miller
fonte
26

Outro erro comum é o tratamento inadequado de exceções. Quando um encadeamento em segundo plano lança uma exceção, se você não o manipular corretamente, poderá não ver o rastreamento da pilha. Ou talvez sua tarefa em segundo plano pare de executar e nunca inicie novamente porque você não conseguiu lidar com a exceção.

Eric Burke
fonte
Sim, e existem boas ferramentas para lidar com isso agora com manipuladores.
Alex Miller
3
Você poderia postar links para artigos ou referências que explicam isso com mais detalhes?
Abhijeet Kashnia 1/11
22

Até que eu fiz uma aula com Brian Goetz Eu não tinha percebido que o não-sincronizada getterde um campo privado transformado através de um sincronizado setteré não garantido para devolver o valor atualizado. Somente quando uma variável é protegida pelo bloco sincronizado nas duas leituras e gravações , você obtém a garantia do valor mais recente da variável.

public class SomeClass{
    private Integer thing = 1;

    public synchronized void setThing(Integer thing)
        this.thing = thing;
    }

    /**
     * This may return 1 forever and ever no matter what is set
     * because the read is not synched
     */
    public Integer getThing(){
        return thing;  
    }
}
John Russell
fonte
5
Nas JVMs posteriores (1.5 e adiante, acho), o uso de volátil também corrigirá isso.
James Schek
2
Não necessariamente. volátil fornece o valor mais recente, impedindo o retorno de 1 para sempre, mas não fornece bloqueio. É perto, mas não é o mesmo.
John Russell
1
@JohnRussell Eu pensei que o volátil garante um relacionamento antes do acontecimento. isso não é "travar"? "Uma gravação em uma variável volátil (§8.3.1.4) v sincroniza - com todas as leituras subsequentes de v por qualquer encadeamento (onde subsequente é definido de acordo com a ordem de sincronização)."
Shawn
15

Pensando que você está escrevendo código de thread único, mas usando estática mutável (incluindo singletons). Obviamente eles serão compartilhados entre os threads. Isso acontece surpreendentemente com frequência.

Tom Hawtin - linha de orientação
fonte
3
Sim, de fato! A estática mutável interrompe o confinamento do encadeamento. Surpreendentemente, nunca encontrei nada sobre essa armadilha no JCiP ou no CPJ.
Julien Chastang
Eu espero que isso seja óbvio para as pessoas que fazem programação simultânea. O estado global deve ser o primeiro lugar para verificar a segurança da thread.
Gtrak
1
@ Gary Thing é que eles não estão pensando que estão fazendo programação simultânea.
Tom Hawtin - tackline
15

As chamadas de método arbitrário não devem ser feitas a partir de blocos sincronizados.

Dave Ray mencionou isso em sua primeira resposta e, de fato, também encontrei um impasse relacionado à invocação de métodos para ouvintes de um método sincronizado. Acho que a lição mais geral é que as chamadas de método não devem ser feitas "in the wild" de dentro de um bloco sincronizado - você não tem idéia se a chamada será de longa duração, resultará em impasse ou qualquer outra coisa.

Nesse caso, e geralmente em geral, a solução era reduzir o escopo do bloco sincronizado para proteger apenas uma seção privada crítica do código.

Além disso, como agora acessávamos a coleção de ouvintes fora de um bloco sincronizado, a alteramos para uma coleção de copiar na gravação. Ou poderíamos simplesmente fazer uma cópia defensiva da coleção. O ponto é que geralmente existem alternativas para acessar com segurança uma coleção de objetos desconhecidos.

Scott Bale
fonte
13

O bug mais recente relacionado à concorrência que encontrei foi um objeto que, em seu construtor, criou um ExecutorService, mas quando o objeto não era mais referenciado, ele nunca havia desligado o ExecutorService. Assim, durante um período de semanas, milhares de threads vazaram, causando o travamento do sistema. (Tecnicamente, não travou, mas parou de funcionar corretamente, enquanto continuava sendo executado.)

Tecnicamente, suponho que esse não seja um problema de simultaneidade, mas é um problema relacionado ao uso das bibliotecas java.util.concurrency.

Eddie
fonte
11

A sincronização desequilibrada, particularmente no Maps, parece ser um problema bastante comum. Muitas pessoas acreditam que a sincronização de put em um mapa (não um ConcurrentMap, mas digamos um HashMap) e não a sincronização de get é suficiente. No entanto, isso pode levar a um loop infinito durante o re-hash.

O mesmo problema (sincronização parcial) pode ocorrer em qualquer lugar em que você tenha compartilhado o estado com leituras e gravações.

Alex Miller
fonte
11

Encontrei um problema de simultaneidade com os Servlets, quando há campos mutáveis ​​que serão definidos por cada solicitação. Mas há apenas uma instância de servlet para todas as solicitações, portanto, isso funcionou perfeitamente em um ambiente de usuário único, mas quando mais de um usuário solicitou, ocorreram resultados imprevisíveis do servlet.

public class MyServlet implements Servlet{
    private Object something;

    public void service(ServletRequest request, ServletResponse response)
        throws ServletException, IOException{
        this.something = request.getAttribute("something");
        doSomething();
    }

    private void doSomething(){
        this.something ...
    }
}
Ludwig Wensauer
fonte
10

Não é exatamente um bug, mas o pior pecado é fornecer uma biblioteca que você pretende que outras pessoas usem, mas não declarar quais classes / métodos são seguros para threads e quais devem ser chamados apenas de um único thread, etc.

Mais pessoas devem usar as anotações de simultaneidade (por exemplo, @ThreadSafe, @GuardedBy etc) descritas no livro de Goetz.

Neil Bartlett
fonte
9

Meu maior problema sempre foram os impasses, causados ​​principalmente por ouvintes que são acionados com um bloqueio. Nesses casos, é realmente fácil obter o bloqueio invertido entre dois threads. No meu caso, entre uma simulação em execução em um thread e uma visualização da simulação em execução no thread da interface do usuário.

EDIT: Movida a segunda parte para separar a resposta.

Dave Ray
fonte
Você pode dividir a última em uma resposta separada? Vamos mantê-lo 1 por post. Estes são dois realmente bons.
Alex Miller
9

Iniciar um encadeamento no construtor de uma classe é problemático. Se a classe for estendida, o encadeamento poderá ser iniciado antes da execução do construtor da subclasse .

cinza
fonte
8

Classes mutáveis ​​em estruturas de dados compartilhadas

Thread1:
    Person p = new Person("John");
    sharedMap.put("Key", p);
    assert(p.getName().equals("John");  // sometimes passes, sometimes fails

Thread2:
    Person p = sharedMap.get("Key");
    p.setName("Alfonso");

Quando isso acontece, o código é muito mais complexo que este exemplo simplificado. Replicar, encontrar e corrigir o erro é difícil. Talvez isso pudesse ser evitado se pudéssemos marcar certas classes como imutáveis ​​e certas estruturas de dados como apenas mantendo objetos imutáveis.

Steve McLeod
fonte
8

A sincronização em uma literal ou constante de cadeia definida por uma literal de cadeia é (potencialmente) um problema, pois a literal de cadeia é internada e será compartilhada por qualquer outra pessoa na JVM usando a mesma literal de cadeia. Sei que esse problema surgiu em servidores de aplicativos e outros cenários de "contêiner".

Exemplo:

private static final String SOMETHING = "foo";

synchronized(SOMETHING) {
   //
}

Nesse caso, qualquer pessoa que use a string "foo" para bloquear está compartilhando o mesmo bloqueio.

Alex Miller
fonte
Potencialmente está bloqueado. O problema é que a semântica em WHEN Strings é internada é indefinida (ou, IMNSHO, subdefinida). Uma constante de "foo" no tempo do compilador é internada; "foo" proveniente de uma interface de rede é internada somente se você o fizer.
Kirk Wylie
Certo, é por isso que eu usei especificamente uma constante literal de string, que é garantida para ser internada.
Alex Miller
8

Acredito que no futuro o principal problema com Java será a (falta de) garantia de visibilidade para os construtores. Por exemplo, se você criar a seguinte classe

class MyClass {
    public int a = 1;
}

e depois é só ler a propriedade das MyClass um de outro segmento, MyClass.a poderia ser 0 ou 1, dependendo da implementação e o humor do JavaVM. Hoje, as chances de 'a' ser 1 são muito altas. Mas em futuras máquinas NUMA, isso pode ser diferente. Muitas pessoas não estão cientes disso e acreditam que não precisam se preocupar com o multiencadeamento durante a fase de inicialização.

Tim Jansen
fonte
Acho isso levemente surpreendente, mas sei que você é um cara esperto, Tim, então vou levá-lo sem referência. :) No entanto, se um fosse final, isso não seria uma preocupação, correto? Você seria vinculado pela semântica de congelamento final durante a construção?
21180 Alex Miller
Ainda encontro coisas no JMM que me surpreendem, então não confio em mim, mas tenho certeza disso. Veja também cs.umd.edu/~pugh/java/memoryModel/… . Se o campo fosse final, não seria um problema, pois seria visível após a fase de inicialização.
Tim Jansen
2
Isso é apenas um problema, se a referência da instância criada recentemente já estiver em uso antes que o construtor retorne / termine. Por exemplo, a classe se registra durante a construção em um pool público e outros threads começam a acessá-lo.
ReneS 12/03/09
3
MyClass.a indica acesso estático e 'a' não é um membro estático de MyClass. Fora isso, é como afirma o 'ReneS', isso é apenas um problema se uma referência ao objeto incompleto vazar, como adicionar 'this' a algum mapa externo no construtor, por exemplo.
Markus Jevring
7

O erro mais estúpido que cometo com frequência é esquecer de sincronizar antes de chamar notify () ou wait () em um objeto.

Dave Ray
fonte
8
Diferente da maioria dos problemas de simultaneidade, não é fácil encontrá-lo? Pelo menos você obtém uma IllegalMonitorStateException aqui ... #
Outlaw Programmer
Felizmente, é muito fácil de encontrar ... mas ainda é um erro estúpido que os resíduos meu tempo mais do que deveria :)
Dave Ray
7

Usando um "novo Object ()" local como mutex.

synchronized (new Object())
{
    System.out.println("sdfs");
}

Isso é inútil.

Ludwig Wensauer
fonte
2
Isso provavelmente é inútil, mas o ato de sincronizar faz algumas coisas interessantes ... Certamente, criar um novo Objeto toda vez é um desperdício total.
TREE
4
Não é inútil. É barreira de memória sem trava.
David Roussel
1
@ David: o único problema - JVM pode otimizá-lo através da remoção de tal bloqueio em tudo
yetanothercoder
@insighter Vejo que sua opinião é compartilhada ibm.com/developerworks/java/library/j-jtp10185/index.html Concordo que é uma coisa boba de se fazer, pois você não sabe quando sua barreira de memórias será sincronizada, estava apenas apontando que estava fazendo mais que nada.
David Roussel
7

Outro problema comum de "simultaneidade" é usar código sincronizado quando não for necessário. Por exemplo, ainda vejo programadores usando StringBufferou mesmo java.util.Vector(como variáveis ​​locais do método).

Kutzi
fonte
1
Isso não é um problema, mas desnecessário, porque informa à JVM que sincroniza os dados com a memória global e, portanto, pode executar mal em vários cpus, mesmo assim, ninguém usa o bloco de sincronização de maneira simultânea.
ReneS 12/03/09
6

Vários objetos protegidos por bloqueio, mas geralmente são acessados ​​sucessivamente. Ocorremos em alguns casos em que os bloqueios são obtidos por código diferente em diferentes ordens, resultando em conflito.

Brendan Cashman
fonte
5

Não percebendo que a thisclasse interna não é thisa classe externa. Normalmente, em uma classe interna anônima que implementa Runnable. O problema principal é que, como a sincronização faz parte de todos os Objects, não há efetivamente nenhuma verificação de tipo estático. Eu já vi isso pelo menos duas vezes na usenet, e também aparece na concorrência simultânea de Brian Goetz'z Java na prática.

Os fechamentos de BGGA não sofrem com isso, pois não há thisfechamento (faz thisreferência à classe externa). Se você usar thisobjetos não como bloqueios, ele contorna esse problema e outros.

Tom Hawtin - linha de orientação
fonte
3

Uso de um objeto global, como uma variável estática para bloqueio.

Isso leva a um desempenho muito ruim por causa da contenção.

kohlerm
fonte
Bem, às vezes, às vezes não. Se apenas seria tão fácil ...
gimpf
Supondo que o encadeamento ajude a aumentar o desempenho para o problema fornecido, ele sempre diminui o desempenho assim que mais de um encadeamento acessa o código protegido pelo bloqueio.
kohlerm
3

Honestamente? Antes do advento java.util.concurrent, o problema mais comum que eu encontrava rotineiramente era o que chamo de "debulha de threads": aplicativos que usam threads para simultaneidade, mas geram muitos deles e acabam debatendo.

Brian Clapper
fonte
Você está sugerindo que tem mais problemas agora que o java.util.concurrent está disponível?
Andrzej Doyle