5

Di seguito è riportato un banale programma java. Ha un contatore chiamato "cnt" che viene incrementato e quindi aggiunto a un elenco chiamato "monitor". "cnt" viene incrementato da più thread e i valori vengono aggiunti a "monitor" da più thread.Concorrenza in Java utilizzando blocchi sincronizzati che non danno risultati previsti

Alla fine del metodo "go()", cnt e monitor.size() devono avere lo stesso valore, ma non lo fanno. monitor.size() ha il valore corretto.

Se si modifica il codice rimuovendo il commento da uno dei blocchi sincronizzati commentati e commentando quello attualmente non commentato, il codice produce i risultati previsti. Inoltre, se imposti il ​​conteggio dei thread (THREAD_COUNT) su 1, il codice produce i risultati previsti.

Questo può essere riprodotto solo su una macchina con più core reali.

public class ThreadTester { 

    private List<Integer> monitor = new ArrayList<Integer>(); 
    private Integer cnt = 0; 
    private static final int NUM_EVENTS = 2313; 
    private final int THREAD_COUNT = 13; 

    public ThreadTester() { 
    } 

    public void go() { 
     Runnable r = new Runnable() { 

      @Override 
      public void run() { 
       for (int ii=0; ii<NUM_EVENTS; ++ii) { 
        synchronized(monitor) { 
         synchronized(cnt) {  // <-- is this synchronized necessary? 
          monitor.add(cnt); 
         } 
//      synchronized(cnt) { 
//       cnt++;  // <-- why does moving the synchronized block to here result in the correct value for cnt? 
//      } 
        } 
        synchronized(cnt) { 
         cnt++;    // <-- why does moving the synchronized block here result in cnt being wrong? 
        } 
       } 
//    synchronized(cnt) { 
//     cnt += NUM_EVENTS; // <-- moving the synchronized block here results in the correct value for cnt, no surprise 
//    } 
      } 

     }; 
     Thread[] threads = new Thread[THREAD_COUNT]; 

     for (int ii=0; ii<THREAD_COUNT; ++ii) { 
      threads[ii] = new Thread(r); 
     } 
     for (int ii=0; ii<THREAD_COUNT; ++ii) { 
      threads[ii].start(); 
     } 
     for (int ii=0; ii<THREAD_COUNT; ++ii) { 
      try { threads[ii].join(); } catch (InterruptedException e) { } 
     } 

     System.out.println("Both values should be: " + NUM_EVENTS*THREAD_COUNT); 
     synchronized (monitor) { 
      System.out.println("monitor.size() " + monitor.size()); 
     } 
     synchronized (cnt) { 
      System.out.println("cnt " + cnt); 
     } 
    } 

    public static void main(String[] args) { 
     ThreadTester t = new ThreadTester(); 
     t.go(); 

     System.out.println("DONE"); 
    }  
} 

risposta

3

Ok diamo un'occhiata alle diverse possibilità si parla:

1.

for (int ii=0; ii<NUM_EVENTS; ++ii) { 
    synchronized(monitor) { 
    synchronized(cnt) {  // <-- is this synchronized necessary? 
     monitor.add(cnt); 
    } 
    synchronized(cnt) { 
     cnt++;  // <-- why does moving the synchronized block to here result in the correct value for cnt? 
    } 
} 

In primo luogo il monitor oggetto è condiviso tra i fili, quindi, ottenere un blocco su di esso (che è ciò che fa sincronizzato) farà in modo che il codice all'interno del blocco verrà eseguito solo da un thread alla volta. Quindi i 2 sincronizzati all'interno di quello esterno non sono necessari, il codice è comunque protetto.

2.

for (int ii=0; ii<NUM_EVENTS; ++ii) { 
    synchronized(monitor) { 
    monitor.add(cnt); 
    } 
    synchronized(cnt) { 
    cnt++;    // <-- why does moving the synchronized block here result in cnt being wrong? 
    } 
} 

Ok questo uno è un po 'difficile. cnt è un oggetto Integer e Java non consente la modifica di un oggetto intero (i numeri interi sono immutabili) anche se il codice suggerisce che questo è ciò che sta accadendo qui. Ma ciò che accadrà sarà che cnt ++ creerà un nuovo intero con il valore cnt + 1 e sovrascrive cnt. Questo è ciò che fa effettivamente il codice:

synchronized(cnt) { 
    Integer tmp = new Integer(cnt + 1); 
    cnt = tmp; 
} 

Il problema è che, mentre un thread crea un nuovo oggetto cnt mentre tutti gli altri thread sono in attesa di ottenere un blocco su quello vecchio. Il thread ora rilascia il vecchio cnt e tenterà quindi di ottenere un blocco sul nuovo oggetto cnt e ottenerlo mentre un altro thread ottiene un blocco sul vecchio oggetto cnt. All'improvviso 2 thread si trovano nella sezione critica, eseguendo lo stesso codice e causando una condizione di competizione. Da qui vengono i risultati sbagliati.

Se si rimuove il primo blocco sincronizzato (quello con monitor), il risultato diventa ancora più sbagliato perché aumentano le probabilità di una gara.

In generale si dovrebbe provare a utilizzare sincronizzati solo sulle variabili finali per evitare che ciò accada.

+0

L'osservazione che realmente sta succedendo è questa: Integer tmp = new Integer (cnt + 1); era la parte che mi mancava. Ho dimenticato di considerare l'immutabilità e l'autoboxing in questo. – mangotang