2014-04-08 8 views
6

Ho cercato di dimostrare che c'è un bug nell'applicazione creando un semplice test unitario che sta mettendo i valori sulla mappa. Mi aspettavo ConcurrentModificationException, ma tutto quello che ho ottenuto è stato appendere i thread nell'esecutore e non vedo dove sia esattamente il problema.Perché il codice si blocca con HashMap.put() da più thread?

La prova è qui:

@Test 
public void testHashMap() throws Exception { 
    final Random rnd = new Random(); 
    final Map<String, Object> map = new HashMap<>(); 
    ExecutorService executor = Executors.newFixedThreadPool(10); 
    for (int i = 0; i < 100; i++) { 
     final int counter=i; 
     executor.execute(new Runnable() { 
      @Override 
      public void run() { 
       try{ 
        for (int j = 0; j<1000; j++){ 
         map.put(String.valueOf(rnd.nextLong()), new Object()); 
         //map.put("A", new Object()); 
        } 
        System.out.println("Thread "+counter+" finished"); 
       }catch(Exception e){ 
        System.out.println("Thread "+counter+" failed with exception: "); 
        e.printStackTrace(); 
       } 
      } 
     }); 
    } 
    executor.shutdown(); 
    int i = 0; 
    while (!executor.isTerminated()) { 
     i++; 
     Thread.sleep(1000); 
     System.out.println("Waited "+i+" seconds"); 
    } 
} 

So che non dovrei farlo, ma io non capisco perché non ricevo un'eccezione e perché i fili semplicemente appendere là? Quando faccio un semplice put sulla mappa (il codice commentato), poi passa bene.

Ecco l'esempio di output:

Thread 0 finished 
Thread 1 finished 
Thread 4 finished 
Thread 2 finished 
Thread 5 finished 
Thread 7 finished 
Thread 9 finished 
Thread 10 finished 
Thread 13 finished 
Thread 6 finished 
Thread 14 finished 
Thread 8 finished 
Thread 12 finished 
Thread 16 finished 
Thread 19 finished 
Thread 20 finished 
Thread 21 finished 
Thread 26 finished 
Thread 25 finished 
Thread 24 finished 
Thread 28 finished 
Thread 3 finished 
Thread 31 finished 
Thread 30 finished 
Thread 32 finished 
Thread 34 finished 
Thread 35 finished 
Thread 36 finished 
Thread 37 finished 
Thread 38 finished 
Thread 39 finished 
Thread 22 finished 
Thread 27 finished 
Thread 42 finished 
Thread 43 finished 
Thread 41 finished 
Thread 45 finished 
Thread 44 finished 
Thread 47 finished 
Thread 48 finished 
Thread 49 finished 
Waited 1 seconds 
Waited 2 seconds 
Waited 3 seconds 
Waited 4 seconds 
Waited 5 seconds 
...indefinitely 
+1

Non è possibile utilizzare un 'HashMap' senza sincronizzazione esterna. Dovresti passare a usare 'ConcurrentHashMap'. – Gray

+0

So che non dovrei usarlo e perché, ma non capisco perché sto osservando il comportamento del test. – NeplatnyUdaj

+0

Il multithreading inserisce in un 'HashMap' può causare loop infiniti se le catene di elenchi collegate vengono messe in uno stato corrotto dove diventano accidentalmente liste concatenate. –

risposta

8

Perché il codice di appendere con HashMap.put() da più thread?

Non è possibile utilizzare uno HashMap con più thread senza sincronizzazione esterna. Dovresti passare a utilizzare ConcurrentHashMap.

È anche possibile utilizzare Collections.synchronizedMap(new HashMap<>()); ma lo ConcurrentHashMap dovrebbe offrire prestazioni migliori.

Mi aspettavo ConcurrentModificationException, ma tutto ciò che ho ottenuto è stato appendere i thread nell'esecutore e non vedo esattamente il problema.

Stai visualizzando un blocco, perché uno dei fili ha una versione corrotta del HashMap - probabilmente una lista collegata in loop in cui due voci di hash sono collegate tra di loro o qualcosa del genere. Se fai un dump del thread vedrai che gira mentre attraversi le voci HashMap.

Il ConcurrentModificationException si butta solo se la classe HashMap rileva una modifica. Generalmente si tratta di un utilizzo a thread singolo quando (ad esempio) si rimuove una voce chiamando map.remove(...) anziché iterator.remove() durante l'iterazione sulla mappa.

La sincronizzazione ha due aspetti importanti: il blocco del mutex e la sincronizzazione della memoria. Ogni processore ha una propria memoria cache e i thread possono facilmente vedere le viste parzialmente sincronizzate di un oggetto (il tuo HashMap in questo caso) senza una corretta sincronizzazione della memoria.

L'unica cosa che è piuttosto frustrante è che a volte genera un'eccezione, ma il più delle volte si blocca.

In situazioni multithreading, ci sono tonnellate di condizioni di gara per definizione poiché i thread di solito sono eseguiti in parallelo su più processori. È molto difficile prevedere, data la natura parallela dell'ambiente, il tipo di errore.

+0

Questa sembra una buona spiegazione. L'unica cosa abbastanza frustrante è che a volte genera un'eccezione, ma la maggior parte delle volte si blocca. Era una parte del codice poco importante e non l'ho fatto veramente bene e l'ho solo circondato con try-catch. Ora vedo che può portare a un loop infinito. Mai visto che sta arrivando ... – NeplatnyUdaj

+0

In situazioni multithread ci sono tonnellate di condizioni di gara. È molto difficile prevedere, data la natura parallela del codice, il tipo di errore @NeplatnyUdaj. – Gray

3

citazione dalla documentazione per HashMap:

Nota che il comportamento fail-veloce di un iteratore non può essere garantita, poiché è, in generale, impossibile dare garanzie difficili in presenza di modifiche simultanee non sincronizzate. Gli iteratori fail-fast generano ConcurrentModificationException su una base best-effort. Pertanto, sarebbe sbagliato scrivere un programma che dipendesse da questa eccezione per la sua correttezza: il comportamento fail-fast degli iteratori dovrebbe essere usato solo per rilevare i bug.

Si va avanti a raccomandare:

Map m = Collections.synchronizedMap(new HashMap(...)); 

per assicurarsi che la mappa è sincronizzato.

http://docs.oracle.com/javase/7/docs/api/java/util/HashMap.html#put(K, V)

+0

L'eccezione generata non è garantita, quindi non dovresti fare affidamento su di esso –

+0

Grazie. Ma hai qualche indizio perché si blocca per sempre? – NeplatnyUdaj

+0

L'eccezione viene generata se c'è un'iterazione - non c'è iterazione nel codice dell'OP. – assylias

1

HashMap non è thread-safe. Il problema che vedi è esattamente quello che ho visto succedere in alcune app di produzione alcune volte (sfortunatamente ...). Quello che succede è che HashMap.put ha bisogno di apportare modifiche alle strutture di dati interne. Potresti vederlo in modo logico come un'operazione: mettere un oggetto nella mappa. Ma sotto il cofano, potrebbe essere necessario fare vari lavori domestici come ridimensionare il tavolo hash interno.

Questo deve essere fatto atomicamente. Il metodo put potrebbe dover leggere i dati e quindi aggiornarli in base a ciò che trova. Se due fili lo fanno allo stesso tempo, si stanno calpestando l'un l'altro. Immaginate se una di quelle letture coinvolge un ciclo, come il ricorrere agli oggetti in un secchio tabella hash. Se un filo scorre su di esso mentre la tabella viene riorganizzata da un altro thread, può finire in un ciclo infinito.

Breve storia: utilizzare un blocco sincronizzato o Hashtable.