Domanda

Sono stato con ConcurrentMap di Java per una mappa che può essere utilizzato da più thread. Il putIfAbsent è un ottimo metodo ed è molto più facile da leggere / scrivere che usare le operazioni di mappa standard. Ho qualche codice che assomiglia a questo:

ConcurrentMap<String, Set<X>> map = new ConcurrentHashMap<String, Set<X>>();

// ...

map.putIfAbsent(name, new HashSet<X>());
map.get(name).add(Y);

La leggibilità saggio questo è grande ma richiede la creazione di un nuovo HashSet ogni volta, anche se è già nella mappa. Potrei scrivere questo:

if (!map.containsKey(name)) {
    map.putIfAbsent(name, new HashSet<X>());
}
map.get(name).add(Y);

Con questa modifica si perde un po 'di leggibilità, ma non ha bisogno di creare la HashSet ogni volta. Che è meglio in questo caso? Io tendo a fianco con il primo dato che è più leggibile. Il secondo sarebbe meglio e può essere più corretto. Forse c'è un modo migliore per farlo che uno di questi.

Qual è la migliore pratica per l'utilizzo di un putIfAbsent in questo modo?

È stato utile?

Soluzione

La concorrenza è dura. Se avete intenzione di perdere tempo con le mappe concorrenti al posto di blocco semplice, si potrebbe anche andare per esso. In effetti, non fare le ricerche più del necessario.

Set<X> set = map.get(name);
if (set == null) {
    final Set<X> value = new HashSet<X>();
    set = map.putIfAbsent(name, value);
    if (set == null) {
        set = value;
    }
}

(abituale dichiarazione di non responsabilità StackOverflow:.... Fuori della parte superiore della mia testa Non testato Non compilato Etc)

Aggiornamento: metodo computeIfAbsent di default 1.8 ha aggiunto alle ConcurrentMap (e Map che è una specie di interessante perché che l'attuazione sarebbe sbagliato per ConcurrentMap). (E 1.7 aggiunto il <> "operatore diamante".)

Set<X> set = map.computeIfAbsent(name, n -> new HashSet<>());

(Nota, siete responsabili per il filo di sicurezza di eventuali operazioni delle HashSets contenuti nel ConcurrentMap.)

Altri suggerimenti

risposta di Tom è corretta per quanto riguarda l'utilizzo di API va per ConcurrentMap. Un'alternativa che evita di usare putIfAbsent è quello di utilizzare la mappa di calcolo dalle GoogleCollections / Guava MapMaker che auto-popola i valori di una funzione in dotazione e gestisce tutte le thread-sicurezza per voi. Si crea di fatto un solo valore per ogni chiave e se la funzione di creare è costoso, altri thread che chiedono di ottenere la stessa chiave bloccherà fino a quando il valore diventa disponibile.

Modifica da Guava 11, MapMaker è deprecato e viene sostituito con la roba / LocalCache / CacheBuilder Cache. Questo è un po 'più complicata nel suo uso, ma fondamentalmente isomorfi.

È possibile utilizzare MutableMap.getIfAbsentPut(K, Function0<? extends V>) da Eclipse Collezioni (ex GS Collezioni ).

Il vantaggio rispetto chiamando get(), facendo un controllo nullo, e quindi chiamando putIfAbsent() è che saremo calcoliamo solo hashCode della chiave una volta, e trovare il posto giusto nella tabella hash una volta. In ConcurrentMaps come org.eclipse.collections.impl.map.mutable.ConcurrentHashMap, l'attuazione di getIfAbsentPut() è thread-safe e atomico.

import org.eclipse.collections.impl.map.mutable.ConcurrentHashMap;
...
ConcurrentHashMap<String, MyObject> map = new ConcurrentHashMap<>();
map.getIfAbsentPut("key", () -> someExpensiveComputation());

L'attuazione di org.eclipse.collections.impl.map.mutable.ConcurrentHashMap è veramente non-blocking. Mentre ogni sforzo è fatto per non chiamare la funzione di fabbrica inutilmente, c'è ancora una possibilità che sarà chiamato più di una volta durante la contesa.

Questo fatto lo distingue da Java 8 di ConcurrentHashMap.computeIfAbsent(K, Function<? super K,? extends V>) . Il Javadoc per questo metodo afferma:

  

Il metodo intera chiamata viene eseguita atomicamente, così la funzione   viene applicata al massimo una volta per tasto. Alcune operazioni di aggiornamento tentata su   questa mappa da altri thread può essere bloccato mentre calcolo è in   progredire, in modo che il calcolo deve essere breve e semplice ...

. Nota: Sono un committer per Eclipse Collezioni

Per mantenere un valore pre-inizializzato per ogni thread si può migliorare la risposta accettata:

Set<X> initial = new HashSet<X>();
...
Set<X> set = map.putIfAbsent(name, initial);
if (set == null) {
    set = initial;
    initial = new HashSet<X>();
}
set.add(Y);

Recentemente ho usato questo con i valori della mappa AtomicInteger anziché Set.

In 5+ anni, non posso credere che nessuno ha menzionato o pubblicato una soluzione che utilizza ThreadLocal per risolvere questo problema; e molte delle soluzioni in questa pagina non sono threadsafe e sono solo sciatta.

Utilizzando ThreadLocals per questo problema specifico non è solo considerato best practice per la concorrenza, ma anche per ridurre al minimo la creazione di immondizia / oggetto durante il filo contesa. Inoltre, è il codice incredibilmente pulito.

Ad esempio:

private final ThreadLocal<HashSet<X>> 
  threadCache = new ThreadLocal<HashSet<X>>() {
      @Override
      protected
      HashSet<X> initialValue() {
          return new HashSet<X>();
      }
  };


private final ConcurrentMap<String, Set<X>> 
  map = new ConcurrentHashMap<String, Set<X>>();

E la logica attuale ...

// minimize object creation during thread contention
final Set<X> cached = threadCache.get();

Set<X> data = map.putIfAbsent("foo", cached);
if (data == null) {
    // reset the cached value in the ThreadLocal
    listCache.set(new HashSet<X>());
    data = cached;
}

// make sure that the access to the set is thread safe
synchronized(data) {
    data.add(object);
}

La mia approssimazione generico:

public class ConcurrentHashMapWithInit<K, V> extends ConcurrentHashMap<K, V> {
  private static final long serialVersionUID = 42L;

  public V initIfAbsent(final K key) {
    V value = get(key);
    if (value == null) {
      value = initialValue();
      final V x = putIfAbsent(key, value);
      value = (x != null) ? x : value;
    }
    return value;
  }

  protected V initialValue() {
    return null;
  }
}

E come esempio di utilizzo:

public static void main(final String[] args) throws Throwable {
  ConcurrentHashMapWithInit<String, HashSet<String>> map = 
        new ConcurrentHashMapWithInit<String, HashSet<String>>() {
    private static final long serialVersionUID = 42L;

    @Override
    protected HashSet<String> initialValue() {
      return new HashSet<String>();
    }
  };
  map.initIfAbsent("s1").add("chao");
  map.initIfAbsent("s2").add("bye");
  System.out.println(map.toString());
}
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top