2012-09-18 14 views
6

Desidero elaborare un flusso di richieste client. Ogni richiesta ha il suo tipo speciale. Per prima cosa ho bisogno di inizializzare alcuni dati per quel tipo, dopodiché posso iniziare ad elaborare le richieste. Quando il tipo di client arriva per la prima volta, ho appena inizializzato i dati corrispondenti. Successivamente, tutte le seguenti richieste di quel tipo vengono elaborate utilizzando tali dati.Questo codice è un thread sicuro?

Ho bisogno di farlo in un modo sicuro per thread.

Ecco un codice che ho scritto. È thread-safe?

public class Test { 

    private static Map<Integer, Object> clientTypesInitiated = new ConcurrentHashMap<Integer, Object>(); 

    /* to process client request we need to 
    create corresponding client type data. 
    on the first signal we create that data, 
    on the second - we process the request*/ 

    void onClientRequestReceived(int clientTypeIndex) { 
     if (clientTypesInitiated.put(clientTypeIndex, "") == null) { 
      //new client type index arrived, this type was never processed 
      //process data for that client type and put it into the map of types 
      Object clientTypeData = createClientTypeData(clientTypeIndex); 
      clientTypesInitiated.put(clientTypeIndex, clientTypeData); 
     } else { 
      //already existing index - we already have results and we can use them 
      processClientUsingClientTypeData(clientTypesInitiated.get(clientTypeIndex)); 
     } 
    } 

    Object createClientTypeData(int clientIndex) {return new Object();} 

    void processClientUsingClientTypeData(Object clientTypeData) {} 
} 

Da un lato, ConcurrentHashMap non può produrre map.put (A, B) == nulli due volte per lo stesso A. Da altra parte, l'operazione di assegnazione e comparisson non è thread-safe.

Quindi questo codice è ok? In caso contrario, come posso risolverlo?

AGGIORNAMENTO: Ho accettato la risposta di Martin Serrano perché il suo codice è thread-safe e non è soggetto a un doppio problema di inizializzazione. Ma vorrei sottolineare che non ho trovato alcun problema con la mia versione, pubblicato come risposta sotto, e la mia versione non richiede la sincronizzazione.

risposta

2

Questo codice non è thread-safe perché

//already existing index - we already have results and we can use them 
processClientUsingClientTypeData(clientTypesInitiated.get(clientTypeIndex)); 

ha la possibilità di ottenere il "valore" si inserisce temporaneamente nel controllo put.

Questo codice potrebbe essere fatto threadsafe thusly:

public class Test { 

    private static Map<Integer, Object> clientTypesInitiated = new ConcurrentHashMap<Integer, Object>(); 

    /* to process client request we need to 
     create corresponding client type data. 
     on the first signal we create that data, 
     on the second - we process the request*/ 

void onClientRequestReceived(int clientTypeIndex) { 
    Object clientTypeData = clientTypesInitiated.get(clientTypeIndex); 
    if (clientTypeData == null) { 
     synchronized (clientTypesInitiated) { 
      clientTypeData = clientTypesInitiated.get(clientTypeIndex); 
      if (clientTypeData == null) { 
       //new client type index arrived, this type was never processed 
       //process data for that client type and put it into the map of types 
       clientTypeData = createClientTypeData(clientTypeIndex); 
       clientTypesInitiated.put(clientTypeIndex, clientTypeData); 
      } 
     } 
    } 
    processClientUsingClientTypeData(clientTypeData); 
} 

Object createClientTypeData(int clientIndex) {return new Object();} 

void processClientUsingClientTypeData(Object clientTypeData) {} 

}

+0

Penso che sia necessario un 'else' prima di' processClientUsingClientTypeData'. – Gray

+0

Anche questo non è thread-safe! Aspetto: un thread esegue get, confronta con null, passa if, quindi un altro thread fa lo stesso, quindi uno assegna a clientTypeData e alla fine ritorna dal metodo, ma un altro thread sovrascrive il clientTypeData. –

+0

@Krzysztof cosa non è sicuro? come ho detto, _se non ti dispiace a volte eseguire createClientTypeData più di una volta, questo va bene. nel caso che descrivi, il secondo thread sostituirà semplicemente il risultato del primo. presumibilmente dal momento che ogni richiesta potrebbe attivare l'inizializzazione, quindi produrrà sempre gli stessi risultati. se l'inizializzazione è _really_ costoso, è possibile aggiungere più complessità per evitarlo. –

3

No, non penso che sia ancora sicuro per i thread.

È necessario disporre l'operazione di inserimento in un blocco sincronizzato.

Secondo javadoc for ConcurrentHashMap

operazioni di recupero (compresi get) generalmente non ostruire, così possono sovrapporsi con le operazioni di aggiornamento (compresi mettere e togliere).

+0

non è vero per ConcurrentMap, può ancora averlo thread-safe e senza alcuna sincronizzazione, il che rende ConcurrentMap obsoleto. – jdevelop

+0

@jdevelop: Potresti per favore elaborare? – kosa

+0

Dovrebbe usare il metodo putIfAbsent di ConcurrentMap – jdevelop

0

No non lo è, se avete bisogno di tutto il metodo da thread-safe dichiarano come sincronizzato, o utilizzare un blocco sincronizzato a inserire il codice critico.

+0

Perché? È necessario fornire informazioni non solo per rispondere alla semplice domanda. – Gray

+0

per essere thread-safe, per informazioni di base, google è il tuo migliore amico http://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html –

+0

Ti manca il mio punto. _So la risposta. QUINDI le risposte sono per i posteri e per essere google _destinations_ non indicare le persone a google. Le tue risposte non verranno aggiornate se non forniscono ulteriori informazioni. – Gray

2

dovresti usare putIfAbsent qui, la semantica di questa operazione è simile a CAS, ed è certamente atomica. E dal momento che è atomico - allora non hai problemi con incarichi interni e confronti.

Il codice corrente non è thread-safe.

+0

Non mettereIfAbsent richiede la creazione di dati prima di verificare se c'è qualcosa? Per lo più ci sarebbe, quindi comporta molte inizializzazioni inutili. –

+0

Buona idea, ma in effetti, come ha detto Krzysztof, dovrò eseguire la procedura di inizializzazione più volte in questo caso. – KutaBeach

+0

invece di mettere un oggetto reale - metti Futuro e sei al sicuro. – jdevelop

0

Il codice corrente non è thread-safe poiché può verificarsi un cambio di contesto tra l'if e il put successivo. è necessario creare clientTypesInitiated come tipo ConcurrentHashMap e non sulla mappa dell'interfaccia. Quindi utilizzare il metodo putIfAbsent

0

Penso che il codice non funzioni come previsto. Guarda:

void onClientRequestReceived(int clientTypeIndex) { 
    // the line below ensures the existing data is lost even if was not null 
    if (clientTypesInitiated.put(clientTypeIndex, "") == null) { 
     Object clientTypeData = createClientTypeData(clientTypeIndex); 
     clientTypesInitiated.put(clientTypeIndex, clientTypeData); 
    } else { 
     processClientUsingClientTypeData(clientTypesInitiated.get(clientTypeIndex)); 
    } 
} 

Immagino che tu preferisca una chiamata al metodo get.

Io suggerirei un tale approccio:

void onClientRequestReceived(int clientTypeIndex) { 
    Object clientTypeData; 
    synchronized (clientTypesInitiated) { 
     if ((clientTypeData = clientTypesInitiated.get(clientTypeIndex)) == null) { 
      clientTypeData = createClientTypeData(clientTypeIndex); 
      clientTypesInitiated.put(clientTypeIndex, clientTypeData); 
     } 
    } 
    processClientUsingClientTypeData(clientTypeData); 
} 
0

putIfAbsent funziona grande, ma a volte richiederà inizializzazione ripetuto per diverse volte. Il blocco sincronizzato con blocco a doppio controllo probabilmente può risolvere questo problema.

Ma qui è un'altra opzione sulla base di AtomicIntegerArrays:

public class Test { 

    private static Map<Integer, Object> clientTypesInitiated = new ConcurrentHashMap<Integer, Object>(); 

    private static final int CLIENT_TYPES_NUMBER = 10; 
    private static AtomicIntegerArray clientTypesInitStarted = new AtomicIntegerArray(CLIENT_TYPES_NUMBER); 
    private static AtomicIntegerArray clientTypesInitFinished = new AtomicIntegerArray(CLIENT_TYPES_NUMBER); 

    void onClientRequestReceived(int clientTypeIndex) { 

     int isInitStarted = clientTypesInitStarted.getAndIncrement(clientTypeIndex); 
     int isInitFinished = clientTypesInitFinished.get(clientTypeIndex); 

     if (isInitStarted == 0) { 
      //new client type index arrived, this type was never processed 
      //process data for that client type and put it into the map of types 
      Object clientTypeData = createClientTypeData(clientTypeIndex); 
      clientTypesInitiated.put(clientTypeIndex, clientTypeData); 
      clientTypesInitFinished.getAndIncrement(clientTypeIndex); 
     } else if (isInitFinished > 0) { 
      //already existing index - we already have results and we can use them 
      processClientUsingClientTypeData(clientTypesInitiated.get(clientTypeIndex)); 
     } 
    } 

    Object createClientTypeData(int clientIndex) { 
     return new Object(); 
    } 

    void processClientUsingClientTypeData(Object clientTypeData) { 
    } 
} 

Tutte le opinioni?

+0

Penso che dovresti considerare di sviluppare più codice orientato agli oggetti. Usare il riferimento a 'Object' non è una soluzione di bell'aspetto. Creare una classe astratta ClientData e derivarne implementando classi concrete. Crea una classe factory per quelli che li istanziano in base a 'request.class'. Il codice che ha enumerazioni di tipo e indicizzazione in generale è difficile in manutenzione ed estensione. –

+0

Ci scusiamo per il disordine, Krzysztof, Object è solo un manichino da mostrare come ClientData, ovviamente non dovrebbe essere così. Come per le classi - in realtà ogni tipo di client è solo un indice di risorsa, i tipi di client non differiscono affatto. Devo avviare alcune risorse prima di elaborare i client relativi a tale risorsa. Sì, sarebbe meglio scrivere resourceIndex invece di clientType. – KutaBeach

+0

E riferendomi al codice attuale ora, temo che sia difettoso. Guarda: il primo thread viene eseguito in 'if'. Il secondo attraversa il metodo, salta prima "se".Poi fallisce al secondo confronto e anche ** salta 'else if' **, quindi non fa nulla. –

1

ConcurrentHashMap è stato creato per non utilizzare il blocco sincronizzato, sebbene utilizzarlo come blocco per il blocco sincronizzato sia ok, ma non efficiente. Se è necessario utilizzare ConcurrentHashMap, l'utilizzo corretto è il seguente;

private static Map<Integer, Object> clientTypesInitiated = new ConcurrentHashMap<Integer, Object>(); 

void onClientRequestReceived(int clientTypeIndex) { 
    Object clientTypeData = clientTypesInitiated.get(clientTypeIndex); 
    if (clientTypeData == null) { 
     Object newClientTypeData = createClientTypeData(clientTypeIndex); 
     clientTypeData = clientTypesInitiated.putIfAbsent(clientTypeIndex, newClientTypeData); 
     // if clientTypeData is null, then put successful, otherwise, use the old one 
     if (clientTypeData == null) { 
      clientTypeData = newClientTypeData; 
     } 
    } 
    processClientUsingClientTypeData(clientTypeData); 
} 

Per essere onesti, il codice di cui sopra potrebbe creare clientTypeData più volte (a causa della condizione di competizione), anche se non usa synchronized blocco. Pertanto, dovresti misurare quanto è costoso creare clientTypeData e se è così costoso che oscura non usando il blocco synchronized, non hai nemmeno bisogno di usare ConcurrentHashMap. Utilizzare il blocco normale HashMap e synchronized.