Guida alla parola chiave sincronizzata in Java

1. Panoramica

Questo rapido articolo sarà un'introduzione all'uso del blocco sincronizzato in Java.

In poche parole, in un ambiente multi-thread, si verifica una condizione di competizione quando due o più thread tentano di aggiornare i dati condivisi modificabili allo stesso tempo. Java offre un meccanismo per evitare condizioni di competizione sincronizzando l'accesso dei thread ai dati condivisi.

Un pezzo di logica contrassegnato con synchronized diventa un blocco sincronizzato, consentendo l'esecuzione di un solo thread alla volta .

2. Perché la sincronizzazione?

Consideriamo una tipica condizione di gara in cui calcoliamo la somma e più thread eseguono il metodo calcola () :

public class BaeldungSynchronizedMethods { private int sum = 0; public void calculate() { setSum(getSum() + 1); } // standard setters and getters } 

E scriviamo un semplice test:

@Test public void givenMultiThread_whenNonSyncMethod() { ExecutorService service = Executors.newFixedThreadPool(3); BaeldungSynchronizedMethods summation = new BaeldungSynchronizedMethods(); IntStream.range(0, 1000) .forEach(count -> service.submit(summation::calculate)); service.awaitTermination(1000, TimeUnit.MILLISECONDS); assertEquals(1000, summation.getSum()); }

Stiamo semplicemente utilizzando un ExecutorService con un pool di 3 thread per eseguire il calcolo () 1000 volte.

Se dovessimo eseguirlo in serie, l'output atteso sarebbe 1000, ma la nostra esecuzione multi-thread fallisce quasi ogni volta con un output effettivo incoerente, ad esempio:

java.lang.AssertionError: expected: but was: at org.junit.Assert.fail(Assert.java:88) at org.junit.Assert.failNotEquals(Assert.java:834) ...

Questo risultato ovviamente non è inaspettato.

Un modo semplice per evitare la race condition consiste nel rendere l'operazione thread-safe utilizzando la parola chiave synchronized .

3. La parola chiave sincronizzata

La parola chiave sincronizzata può essere utilizzata su diversi livelli:

  • Metodi di istanza
  • Metodi statici
  • Blocchi di codice

Quando utilizziamo un blocco sincronizzato , internamente Java utilizza un monitor noto anche come blocco del monitor o blocco intrinseco, per fornire la sincronizzazione. Questi monitor sono associati a un oggetto, quindi tutti i blocchi sincronizzati dello stesso oggetto possono avere un solo thread che li esegue contemporaneamente.

3.1. Metodi di istanza sincronizzati

Aggiungi semplicemente la parola chiave synchronized nella dichiarazione del metodo per sincronizzare il metodo:

public synchronized void synchronisedCalculate() { setSum(getSum() + 1); }

Si noti che una volta sincronizzato il metodo, il test case viene superato, con l'output effettivo pari a 1000:

@Test public void givenMultiThread_whenMethodSync() { ExecutorService service = Executors.newFixedThreadPool(3); SynchronizedMethods method = new SynchronizedMethods(); IntStream.range(0, 1000) .forEach(count -> service.submit(method::synchronisedCalculate)); service.awaitTermination(1000, TimeUnit.MILLISECONDS); assertEquals(1000, method.getSum()); }

I metodi di istanza vengono sincronizzati sull'istanza della classe proprietaria del metodo. Ciò significa che solo un thread per istanza della classe può eseguire questo metodo.

3.2. Sincronizzato STATI c Metodi

I metodi statici sono sincronizzati proprio come i metodi di istanza:

 public static synchronized void syncStaticCalculate() { staticSum = staticSum + 1; }

Questi metodi sono sincronizzati sulla classe oggetto associato alla classe e poiché solo una classe oggetto esiste per JVM per classe, solo thread possono eseguire all'interno di uno statico sincronizzato metodo per classe, indipendentemente dal numero di istanze esso contiene.

Proviamolo:

@Test public void givenMultiThread_whenStaticSyncMethod() { ExecutorService service = Executors.newCachedThreadPool(); IntStream.range(0, 1000) .forEach(count -> service.submit(BaeldungSynchronizedMethods::syncStaticCalculate)); service.awaitTermination(100, TimeUnit.MILLISECONDS); assertEquals(1000, BaeldungSynchronizedMethods.staticSum); }

3.3. Blocchi sincronizzati all'interno dei metodi

A volte non vogliamo sincronizzare l'intero metodo ma solo alcune istruzioni al suo interno. Ciò può essere ottenuto applicando sincronizzato a un blocco:

public void performSynchronisedTask() { synchronized (this) { setCount(getCount()+1); } }

Testiamo la modifica:

@Test public void givenMultiThread_whenBlockSync() { ExecutorService service = Executors.newFixedThreadPool(3); BaeldungSynchronizedBlocks synchronizedBlocks = new BaeldungSynchronizedBlocks(); IntStream.range(0, 1000) .forEach(count -> service.submit(synchronizedBlocks::performSynchronisedTask)); service.awaitTermination(100, TimeUnit.MILLISECONDS); assertEquals(1000, synchronizedBlocks.getCount()); }

Si noti che abbiamo passato un parametro this al blocco sincronizzato . Questo è l'oggetto monitor, il codice all'interno del blocco viene sincronizzato sull'oggetto monitor. In poche parole, solo un thread per oggetto monitor può essere eseguito all'interno di quel blocco di codice.

Nel caso in cui il metodo sia statico , passeremo il nome della classe al posto del riferimento all'oggetto. E la classe sarebbe un monitor per la sincronizzazione del blocco:

public static void performStaticSyncTask(){ synchronized (SynchronisedBlocks.class) { setStaticCount(getStaticCount() + 1); } }

Testiamo il blocco all'interno del metodo statico :

@Test public void givenMultiThread_whenStaticSyncBlock() { ExecutorService service = Executors.newCachedThreadPool(); IntStream.range(0, 1000) .forEach(count -> service.submit(BaeldungSynchronizedBlocks::performStaticSyncTask)); service.awaitTermination(100, TimeUnit.MILLISECONDS); assertEquals(1000, BaeldungSynchronizedBlocks.getStaticCount()); }

3.4. Rientranza

Il blocco dietro i metodi e i blocchi sincronizzati è rientrante. Cioè, il thread corrente può acquisire lo stesso blocco sincronizzato più e più volte mentre lo tiene premuto:

Object lock = new Object(); synchronized (lock) { System.out.println("First time acquiring it"); synchronized (lock) { System.out.println("Entering again"); synchronized (lock) { System.out.println("And again"); } } }

Come mostrato sopra, mentre siamo in un blocco sincronizzato , possiamo acquisire ripetutamente lo stesso blocco del monitor.

4. Conclusione

In questo breve articolo, abbiamo visto diversi modi di utilizzare la parola chiave sincronizzata per ottenere la sincronizzazione dei thread.

Abbiamo anche esplorato come una condizione di competizione può influire sulla nostra applicazione e come la sincronizzazione ci aiuta a evitarlo. Per ulteriori informazioni sulla sicurezza dei thread utilizzando i blocchi in Java, fare riferimento al nostro articolo java.util.concurrent.Locks .

Il codice completo per questo tutorial è disponibile su GitHub.