È ConcurrentHashMap.get () garantito per vedere un ConcurrentHashMap.put precedente () di thread diverso?

StackOverflow https://stackoverflow.com/questions/1770166

Domanda

è ConcurrentHashMap.get() garantito per vedere un precedente ConcurrentHashMap.put() dal thread diverso? La mia aspettativa è che è a dire, e leggendo i JavaDocs sembra indicare così, ma io sono il 99% convinto che la realtà è diversa. Sul mio server di produzione il seguente sembra per accadere. (Ho preso con la registrazione.)

esempio di codice Pseudo:

static final ConcurrentHashMap map = new ConcurrentHashMap();
//sharedLock is key specific.  One map, many keys.  There is a 1:1 
//      relationship between key and Foo instance.
void doSomething(Semaphore sharedLock) {
    boolean haveLock = sharedLock.tryAcquire(3000, MILLISECONDS);

    if (haveLock) {
        log("Have lock: " + threadId);
        Foo foo = map.get("key");
        log("foo=" + foo);

        if (foo == null) {
            log("New foo time! " + threadId);
            foo = new Foo(); //foo is expensive to instance
            map.put("key", foo);

        } else
            log("Found foo:" + threadId);

        log("foo=" + foo);
        sharedLock.release();

    } else
        log("No lock acquired");
} 

Ciò che sembra accadere è questo:

Thread 1                          Thread 2
 - request lock                    - request lock
 - have lock                       - blocked waiting for lock
 - get from map, nothing there
 - create new foo
 - place new foo in map
 - logs foo.toString()
 - release lock
 - exit method                     - have lock
                                   - get from map, NOTHING THERE!!! (Why not?)
                                   - create new foo
                                   - place new foo in map
                                   - logs foo.toString()
                                   - release lock
                                   - exit method

Quindi, la mia uscita è simile al seguente:

Have lock: 1    
foo=null
New foo time! 1
foo=foo@cafebabe420
Have lock: 2    
foo=null
New foo time! 2
foo=foo@boof00boo    

Il secondo filo non vede immediatamente put! Perché? Sul mio sistema di produzione, ci sono più discussioni e ho visto un solo thread, il primo che segue immediatamente thread 1, avere un problema.

Ho anche provato riducendo il livello di concorrenza sul ConcurrentHashMap a 1, non che dovrebbe importare. Per esempio:.

static ConcurrentHashMap map = new ConcurrentHashMap(32, 1);

Dove sto andando male? La mia aspettativa? O c'è qualche bug nel mio codice (il vero software, non il sopra) che è la causa? Sono andato su di esso più volte e sono sicuro al 99% che sto gestire il blocco in modo corretto. Non riesco nemmeno a capire un bug nel ConcurrentHashMap o la JVM. Si prega di salvare da me stesso.

Gorey specifiche che potrebbero essere rilevanti:

  • quad-core a 64 bit Xeon (DL380 G5)
  • RHEL4 (Linux mysvr 2.6.9-78.0.5.ELsmp #1 SMP ... x86_64 GNU/Linux)
  • Java 6 (build 1.6.0_07-b06, 64-Bit Server VM (build 10.0-b23, mixed mode))
È stato utile?

Soluzione

Alcune buone risposte qui, ma per quanto ne so nessuno ha effettivamente fornito una risposta canonica alla domanda posta: "È ConcurrentHashMap.get () garantito per vedere un precedente ConcurrentHashMap.put () di thread diverso" . Quelli che hanno detto di sì non hanno fornito una fonte.

Quindi: sì, è garantito. Fonte (vedere la sezione 'Memory Coerenza Properties'):

  

azioni in un filo prima di effettuare un oggetto in qualsiasi raccolta concorrente accadere, prima successive azioni all'accesso o rimozione di tale elemento dalla raccolta in un altro thread.

Altri suggerimenti

La questione della creazione di un oggetto costoso da creare in una cache basata su un fallimento della ricerca nella cache è noto problema. E per fortuna questo era già stato attuato.

È possibile utilizzare MapMaker Google Collecitons . Basta dargli un callback che crea l'oggetto, e se il codice client cerca nella mappa e la mappa è vuota, il callback viene chiamata e il risultato messo nella mappa.

MapMaker javadocs ...

 ConcurrentMap<Key, Graph> graphs = new MapMaker()
       .concurrencyLevel(32)
       .softKeys()
       .weakValues()
       .expiration(30, TimeUnit.MINUTES)
       .makeComputingMap(
           new Function<Key, Graph>() {
             public Graph apply(Key key) {
               return createExpensiveGraph(key);
             }
           });

A proposito, nel tuo esempio originale non v'è alcun vantaggio di utilizzare un ConcurrentHashMap, come stai fissando ogni accesso, perché non basta usare un HashMap normale all'interno della vostra sezione bloccato?

Una cosa da considerare, è se le chiavi sono uguali e hanno codici hash identici in entrambe le volte della chiamata "get". Se sono solo Strings allora sì, non c'è sarà un problema qui. Ma come non avete dato il tipo generico delle chiavi, e si è eliso dettagli "poco importanti" nel pseudocodice, mi chiedo se si sta utilizzando un'altra classe come chiave.

In ogni caso, si consiglia di registrare inoltre il codice hash delle chiavi utilizzate per le ottiene / mette in discussioni 1 e 2. Se questi sono diversi, avete il vostro problema. Si noti inoltre che key1.equals(key2) deve essere vero; questo non è qualcosa che si può accedere in via definitiva, ma se le chiavi non sono classi finali sarebbe valsa la pena di registrazione il nome completo della classe, poi guardando le equals () metodo per quella classe / classi per vedere se è possibile che il seconda chiave potrebbe essere considerato diverso da prima.

E per rispondere alla tua titolo - sì, ConcurrentHashMap.get () è garantito per vedere qualsiasi put precedente (), dove "precedente" significa che c'è un accade-prima relazione tra i due, come specificato per il modello di memoria di Java. (Per la ConcurrentHashMap in particolare, questo è essenzialmente ciò che ci si aspetterebbe, con l'avvertenza che non si può essere in grado di dire che avviene prima se entrambi i thread eseguono al "esattamente nello stesso momento" su diversi core. Nel tuo caso, però , si dovrebbe assolutamente vedere il risultato della put () in filo 2).

Se un thread mette un valore nella mappa di hash concomitanti poi qualche altro filo che recupera il valore della carta è garantita per visualizzare i valori inseriti dal thread precedente.

Questo problema è stato chiarito in "Java Concurrency in Practice" di Joshua Bloch.

Citando dal testo: -

  

Le collezioni della biblioteca thread-safe offrono le seguenti garanzie pubblicazione di sicurezza, anche se il javadoc è inferiore chiara sul tema:

     
      
  • Effettuazione di una chiave o un valore in una Hashtable, synchronizedMap o Concurrent-Map sicuro pubblica a qualsiasi altro filo che recupera dalla mappa (direttamente o tramite un iteratore);
  •   

Non credo che il problema è in "ConcurrentHashMap", ma piuttosto da qualche parte nel codice o circa il ragionamento circa il vostro codice. Non riesco a individuare l'errore nel codice di cui sopra (forse noi non vediamo la parte cattiva?).

Ma per rispondere alla tua domanda "Is ConcurrentHashMap.get () garantito per vedere un precedente ConcurrentHashMap.put () di thread diverso?" Ho messo insieme un piccolo programma di test.

In breve: No, ConcurrentHashMap è OK

Se la mappa è scritto male il seguente programma shoukd stampare "l'accesso Bad!" almeno di tanto in tanto. Si lancia 100 thread con 100000 chiamate al metodo di cui sopra. Ma la stampa "Tutto ok!".

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class Test {
    private final static ConcurrentHashMap<String, Test> map = new ConcurrentHashMap<String, Test>();
    private final static Semaphore lock = new Semaphore(1);
    private static int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(100);
        List<Callable<Boolean>> testCalls = new ArrayList<Callable<Boolean>>();
        for (int n = 0; n < 100000; n++)
            testCalls.add(new Callable<Boolean>() {
                @Override
                public Boolean call() throws Exception {
                    doSomething(lock);
                    return true;
                }
            });
        pool.invokeAll(testCalls);
        pool.shutdown();
        pool.awaitTermination(5, TimeUnit.SECONDS);
        System.out.println("All ok!");
    }

    static void doSomething(Semaphore lock) throws InterruptedException {
        boolean haveLock = lock.tryAcquire(3000, TimeUnit.MILLISECONDS);

        if (haveLock) {
            Test foo = map.get("key");
            if (foo == null) {
                foo = new Test();
                map.put("key", new Test());
                if (counter > 0)
                    System.err.println("Bad access!");
                counter++;
            }
            lock.release();
        } else {
            System.err.println("Fail to lock!");
        }
    }
}

Aggiornamento: putIfAbsent() è logicamente corretto qui, ma non evita il problema della creazione di un unico Foo nel caso in cui la chiave non è presente. Si crea sempre il Foo, anche se non finisce per mettere nella mappa. La risposta di David Roussel è buona, supponendo che si può accettare la dipendenza di Google Collezioni nella vostra app.


Forse mi manca qualcosa di ovvio, ma perché stai guardia la mappa con un semaforo? ConcurrentHashMap (CHM) è thread-safe (supponendo che è sicuro pubblicata, che è qui). Se si sta cercando di ottenere atomica "mettere se non già in là", l'uso chm. putIfAbsent() . Se avete bisogno di più invarianti complciated dove i contenuti della mappa non possono cambiare, probabilmente è necessario utilizzare un HashMap regolare e sincronizzarlo come al solito.

Per rispondere alla tua domanda in modo più diretto:. Una volta che le put ritorni, il valore si inserisce nella mappa è garantito per essere visto dal filo successivo che lo cerca

Nota a margine, solo un +1 ad alcuni altri commenti circa mettere il rilascio del semaforo in un fine.

if (sem.tryAcquire(3000, TimeUnit.MILLISECONDS)) {
    try {
        // do stuff while holding permit    
    } finally {
        sem.release();
    }
}

Stiamo assistendo ad una interessante manifestazione del modello di memoria di Java? In quali condizioni si registri svuotate alla memoria principale? Penso che sia garantito che, se due thread sincronizzano sullo stesso oggetto Allora vedranno una vista di memoria coerente.

Non so che cosa Semphore fa internamente, quasi ovviamente, deve fare un po 'di sincronizzazione, ma sappiamo che?

Che cosa succede se si fa

synchronize(dedicatedLockObject)

invece che acquisendo semaforo?

Perché il bloccaggio di una mappa di hash concomitante? Def. suo filo sicuro. Se c'è un problema, la sua nel codice di blocco. Ecco perché abbiamo filo pacchetti di sicurezza in Java Il modo migliore per eseguire il debug di questo è con la sincronizzazione di barriera.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top