Bloco sincronizado de Java vs. Collections.synchronizedMap

85

O código a seguir está configurado para sincronizar corretamente as chamadas synchronizedMap?

public class MyClass {
  private static Map<String, List<String>> synchronizedMap = Collections.synchronizedMap(new HashMap<String, List<String>>());

  public void doWork(String key) {
    List<String> values = null;
    while ((values = synchronizedMap.remove(key)) != null) {
      //do something with values
    }
  }

  public static void addToMap(String key, String value) {
    synchronized (synchronizedMap) {
      if (synchronizedMap.containsKey(key)) {
        synchronizedMap.get(key).add(value);
      }
      else {
        List<String> valuesList = new ArrayList<String>();
        valuesList.add(value);
        synchronizedMap.put(key, valuesList);
      }
    }
  }
}

Do meu entendimento, eu preciso do bloco sincronizado addToMap()para evitar que outro thread chame remove()ou containsKey()antes de eu fazer a chamada para put()mas eu não preciso de um bloco sincronizado doWork()porque outro thread não pode entrar no bloco sincronizado addToMap()antes de remove()retornar porque eu criei o mapa originalmente com Collections.synchronizedMap(). Isso está correto? Existe uma maneira melhor de fazer isso?

Ryan Ahearn
fonte

Respostas:

90

Collections.synchronizedMap() garante que cada operação atômica que você deseja executar no mapa seja sincronizada.

A execução de duas (ou mais) operações no mapa, entretanto, deve ser sincronizada em um bloco. Então, sim - você está sincronizando corretamente.

Yuval Adam
fonte
26
Acho que seria bom mencionar que isso funciona porque os javadocs declaram explicitamente que synchronizedMap sincroniza no próprio mapa, e não em algum bloqueio interno. Se fosse esse o caso, synchronized (synchronizedMap) não estaria correto.
extraneon
2
@Yuval você poderia explicar sua resposta com um pouco mais de profundidade? Você diz que sychronizedMap faz operações atomicamente, mas então por que você precisaria de seu próprio bloco sincronizado se o syncMap tornasse todas as suas operações atômicas? Seu primeiro parágrafo parece impedir a preocupação com o segundo.
almel
@almel veja minha resposta
Sergey
2
porque é necessário ter bloco sincronizado como o mapa já usa Collections.synchronizedMap()? Não estou entendendo o segundo ponto.
Bimal Sharma
15

Se você estiver usando JDK 6, você pode querer verificar ConcurrentHashMap

Observe o método putIfAbsent nessa classe.

TofuBeer
fonte
13

Existe a possibilidade de um bug sutil em seu código.

[ ATUALIZAÇÃO: Como ele está usando map.remove (), esta descrição não é totalmente válida. Eu perdi esse fato na primeira vez. :( Obrigado ao autor da pergunta por apontar isso. Estou deixando o resto como está, mas alterei a declaração principal para dizer que há potencialmente um bug.]

Em doWork (), você obtém o valor de lista do mapa de uma maneira segura para thread. Posteriormente, no entanto, você acessará essa lista de uma forma insegura. Por exemplo, um thread pode estar usando a lista em doWork () enquanto outro thread invoca synchronizedMap.get (key) .add (value) em addToMap () . Esses dois acessos não estão sincronizados. A regra é que as garantias de thread-safe de uma coleção não se estendem às chaves ou valores que armazenam.

Você pode corrigir isso inserindo uma lista sincronizada no mapa, como

List<String> valuesList = new ArrayList<String>();
valuesList.add(value);
synchronizedMap.put(key, Collections.synchronizedList(valuesList)); // sync'd list

Alternativamente, você pode sincronizar no mapa enquanto acessa a lista em doWork () :

  public void doWork(String key) {
    List<String> values = null;
    while ((values = synchronizedMap.remove(key)) != null) {
      synchronized (synchronizedMap) {
          //do something with values
      }
    }
  }

A última opção limitará um pouco a simultaneidade, mas é um pouco mais clara, IMO.

Além disso, uma nota rápida sobre ConcurrentHashMap. Esta é uma classe realmente útil, mas nem sempre é uma substituição apropriada para HashMaps sincronizados. Citando seus Javadocs,

Esta classe é totalmente interoperável com Hashtable em programas que dependem de sua segurança de thread, mas não de seus detalhes de sincronização .

Em outras palavras, putIfAbsent () é ótimo para inserções atômicas, mas não garante que outras partes do mapa não mudem durante essa chamada; garante apenas atomicidade. Em seu programa de amostra, você está contando com os detalhes de sincronização de (um HashMap sincronizado) para coisas diferentes de put () s.

Última coisa. :) Esta ótima citação de Java Concurrency in Practice sempre me ajuda a projetar programas multithread de depuração.

Para cada variável de estado mutável que pode ser acessada por mais de uma thread, todos os acessos a essa variável devem ser realizados com o mesmo bloqueio mantido.

JLR
fonte
Eu vejo seu ponto sobre o bug se eu estivesse acessando a lista com synchronizedMap.get (). Como estou usando remove (), o próximo acréscimo com essa chave não deveria criar um novo ArrayList e não interferir com o que estou usando no doWork?
Ryan Ahearn
Corrigir! Eu superei totalmente sua remoção.
JLR de
1
Para cada variável de estado mutável que pode ser acessada por mais de uma thread, todos os acessos a essa variável devem ser realizados com o mesmo bloqueio mantido. ---- Eu geralmente adiciono uma propriedade privada que é apenas um novo Object () e uso isso para meus blocos de sincronização. Dessa forma, eu sei que é tudo em bruto para esse contexto. synchronized (objectInVar) {}
AnthonyJClink
11

Sim, você está sincronizando corretamente. Vou explicar isso com mais detalhes. Você deve sincronizar duas ou mais chamadas de método no objeto synchronizedMap apenas no caso de depender dos resultados de chamadas de método anteriores na chamada de método subsequente na sequência de chamadas de método no objeto synchronizedMap. Vamos dar uma olhada neste código:

synchronized (synchronizedMap) {
    if (synchronizedMap.containsKey(key)) {
        synchronizedMap.get(key).add(value);
    }
    else {
        List<String> valuesList = new ArrayList<String>();
        valuesList.add(value);
        synchronizedMap.put(key, valuesList);
    }
}

Neste código

synchronizedMap.get(key).add(value);

e

synchronizedMap.put(key, valuesList);

chamadas de método são baseadas no resultado do anterior

synchronizedMap.containsKey(key)

chamada de método.

Se a sequência de chamadas de método não foi sincronizada, o resultado pode estar errado. Por exemplo, thread 1está executando o método addToMap()e thread 2está executando o método doWork() A sequência de chamadas de método no synchronizedMapobjeto pode ser a seguinte: Thread 1executou o método

synchronizedMap.containsKey(key)

e o resultado é " true". Depois que o sistema operacional mudou o controle de execução para thread 2e foi executado

synchronizedMap.remove(key)

Depois que o controle de execução foi alternado de volta para o thread 1e foi executado, por exemplo

synchronizedMap.get(key).add(value);

acreditando que o synchronizedMapobjeto contém o keye NullPointerExceptionserá lançado porque synchronizedMap.get(key) retornará null. Se a sequência de chamadas de método no synchronizedMapobjeto não depender dos resultados uma da outra, não será necessário sincronizar a sequência. Por exemplo, você não precisa sincronizar esta sequência:

synchronizedMap.put(key1, valuesList1);
synchronizedMap.put(key2, valuesList2);

Aqui

synchronizedMap.put(key2, valuesList2);

chamada de método não depende dos resultados do anterior

synchronizedMap.put(key1, valuesList1);

chamada de método (não importa se algum thread interferiu entre as duas chamadas de método e, por exemplo, removeu o key1).

Sergey
fonte
4

Isso parece correto para mim. Se eu fosse mudar alguma coisa, pararia de usar o Collections.synchronizedMap () e sincronizaria tudo da mesma forma, só para deixar mais claro.

Além disso, eu substituiria

  if (synchronizedMap.containsKey(key)) {
    synchronizedMap.get(key).add(value);
  }
  else {
    List<String> valuesList = new ArrayList<String>();
    valuesList.add(value);
    synchronizedMap.put(key, valuesList);
  }

com

List<String> valuesList = synchronziedMap.get(key);
if (valuesList == null)
{
  valuesList = new ArrayList<String>();
  synchronziedMap.put(key, valuesList);
}
valuesList.add(value);
Paul Tomblin
fonte
3
A única coisa a fazer. Eu não entendo por que devemos usar as Collections.synchronizedXXX()APIs quando ainda temos que sincronizar em algum objeto (que será apenas a coleção em si na maioria dos casos) na lógica de nosso aplicativo de todos os dias
kellogs
3

A forma como você sincronizou está correta. Mas há um porém

  1. O wrapper sincronizado fornecido pela estrutura Collection garante que as chamadas de método, ou seja, adicionar / obter / conter, serão executadas mutuamente exclusivas.

No entanto, no mundo real, você geralmente consulta o mapa antes de inserir o valor. Portanto, você precisaria fazer duas operações e, portanto, um bloco sincronizado é necessário. Portanto, a maneira como você o usou está correta. Contudo.

  1. Você poderia ter usado uma implementação simultânea do Mapa disponível na estrutura da Coleção. O benefício de 'ConcurrentHashMap' é

uma. Ele tem uma API 'putIfAbsent' que faria a mesma coisa, mas de uma maneira mais eficiente.

b. É eficiente: dO CocurrentMap apenas bloqueia as chaves, portanto, não bloqueia todo o mundo do mapa. Onde, como você bloqueou as chaves, bem como os valores.

c. Você poderia ter passado a referência do seu objeto de mapa em algum outro lugar em sua base de código onde você / outro dev em seu tean pode acabar usando-o incorretamente. Ou seja, ele pode apenas adicionar () ou obter () sem bloquear o objeto do mapa. Portanto, a chamada dele não será executada mutuamente exclusiva para o seu bloco de sincronização. Mas usar uma implementação simultânea oferece a você a tranquilidade de saber que ela nunca pode ser usada / implementada incorretamente.

Jai Pandit
fonte
2

Confira as Coleções do Google ' Multimap, por exemplo, página 28 desta apresentação .

Se você não puder usar essa biblioteca por algum motivo, considere usar em ConcurrentHashMapvez de SynchronizedHashMap; tem um putIfAbsent(K,V)método bacana com o qual você pode adicionar atomicamente a lista de elementos se ainda não estiver lá. Além disso, considere usar CopyOnWriteArrayListpara os valores do mapa se seus padrões de uso justificarem.

Barend
fonte