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?
fonte
Collections.synchronizedMap()
? Não estou entendendo o segundo ponto.Se você estiver usando JDK 6, você pode querer verificar ConcurrentHashMap
Observe o método putIfAbsent nessa classe.
fonte
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,
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.
fonte
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
e
chamadas de método são baseadas no resultado do anterior
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 1
está executando o métodoaddToMap()
ethread 2
está executando o métododoWork()
A sequência de chamadas de método nosynchronizedMap
objeto pode ser a seguinte:Thread 1
executou o métodoe o resultado é "
true
". Depois que o sistema operacional mudou o controle de execução parathread 2
e foi executadoDepois que o controle de execução foi alternado de volta para o
thread 1
e foi executado, por exemploacreditando que o
synchronizedMap
objeto contém okey
eNullPointerException
será lançado porquesynchronizedMap.get(key)
retornaránull
. Se a sequência de chamadas de método nosynchronizedMap
objeto 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:Aqui
chamada de método não depende dos resultados do anterior
chamada de método (não importa se algum thread interferiu entre as duas chamadas de método e, por exemplo, removeu o
key1
).fonte
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);
fonte
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 diasA forma como você sincronizou está correta. Mas há um porém
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.
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.
fonte
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
ConcurrentHashMap
vez deSynchronizedHashMap
; tem umputIfAbsent(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 usarCopyOnWriteArrayList
para os valores do mapa se seus padrões de uso justificarem.fonte