Introduzione alla sincronizzazione in Java

La sincronizzazione è una funzione Java che impedisce a più thread di tentare di accedere contemporaneamente alle risorse comunemente condivise. Qui le risorse condivise si riferiscono a contenuti di file esterni, variabili di classe o record di database.

La sincronizzazione è ampiamente utilizzata nella programmazione multithread. "Sincronizzato" è la parola chiave che fornisce al tuo codice la possibilità di consentire a un solo thread di operare su di esso senza interferenze da qualsiasi altro thread durante quel periodo.

Perché abbiamo bisogno della sincronizzazione in Java?

  • Java è un linguaggio di programmazione multithread. Ciò significa che due o più thread possono essere eseguiti contemporaneamente verso il completamento di un'attività. Quando i thread vengono eseguiti contemporaneamente, ci sono alte probabilità che si verifichi uno scenario in cui il codice potrebbe fornire risultati imprevisti.
  • Potresti chiederti che se il multithreading può causare output errati, perché viene considerata una caratteristica importante in Java?
  • Il multithreading rende il codice più veloce eseguendo più thread in parallelo, riducendo così i tempi di esecuzione dei codici e fornendo prestazioni elevate. Tuttavia, l'utilizzo dell'ambiente multithreading genera output imprecisi a causa di una condizione comunemente nota come condizione di competizione.

Che cos'è una condizione di gara?

Quando due o più thread vengono eseguiti in parallelo, tendono ad accedere e modificare le risorse condivise in quel momento. Le sequenze in cui i thread vengono eseguiti sono decise dall'algoritmo di pianificazione dei thread.

Per questo motivo non è possibile prevedere l'ordine in cui i thread verranno eseguiti in quanto è controllato esclusivamente dallo scheduler dei thread. Ciò influisce sull'output del codice e genera output incoerenti. Poiché più thread sono in competizione l'uno con l'altro per completare l'operazione, la condizione viene definita "condizione di gara".

Ad esempio, consideriamo il codice seguente:

Class Modify:
package JavaConcepts;
public class Modify implements Runnable(
private int myVar=0;
public int getMyVar() (
return myVar;
)
public void setMyVar(int myVar) (
this.myVar = myVar;
)
public void increment() (
myVar++;
)
@Override
public void run() (
// TODO Auto-generated method stub
this.increment();
System.out.println("Current thread being executed "+ Thread.currentThread().getName() + "Current Thread value " + this.getMyVar());
)
)
Class RaceCondition:
package JavaConcepts;
public class RaceCondition (
public static void main(String() args) (
Modify mObj = new Modify();
Thread t1 = new Thread(mObj, "thread 1");
Thread t2 = new Thread(mObj, "thread 2");
Thread t3 = new Thread(mObj, "thread 3");
t1.start();
t2.start();
t3.start();
)
)

Nell'eseguire consecutivamente il codice sopra riportato, le uscite saranno le seguenti:

Ourput1:

Thread corrente in esecuzione thread 1 Valore thread corrente 3

Thread corrente in corso di esecuzione thread 3 Valore thread corrente 2

Thread corrente in esecuzione thread 2 Valore thread corrente 3

Output2:

Thread corrente in corso di esecuzione thread 3 Valore thread corrente 3

Thread corrente in esecuzione thread 2 Valore thread corrente 3

Thread corrente in esecuzione thread 1 Valore thread corrente 3

output3:

Thread corrente in esecuzione thread 2 Valore thread corrente 3

Thread corrente in esecuzione thread 1 Valore thread corrente 3

Thread corrente in corso di esecuzione thread 3 Valore thread corrente 3

OUTPUT4:

Thread corrente in esecuzione thread 1 Valore thread corrente 2

Thread corrente in corso di esecuzione thread 3 Valore thread corrente 3

Thread corrente in esecuzione thread 2 Valore thread corrente 2

  • Dall'esempio precedente, puoi concludere che i thread vengono eseguiti in modo casuale e che il valore non è corretto. Secondo la nostra logica, il valore dovrebbe essere incrementato di 1. Tuttavia, qui il valore di output nella maggior parte dei casi è 3 e in alcuni casi è 2.
  • Qui la variabile "myVar" è la risorsa condivisa su cui sono in esecuzione più thread. I thread accedono e modificano il valore di "myVar" contemporaneamente. Vediamo cosa succede se commentiamo gli altri due thread.

L'output in questo caso è:

Il thread corrente in esecuzione thread 1 Valore thread corrente 1

Ciò significa che quando un singolo thread è in esecuzione l'output è come previsto. Tuttavia, quando sono in esecuzione più thread, il valore viene modificato da ciascun thread. Pertanto, è necessario limitare il numero di thread che lavorano su una risorsa condivisa a un singolo thread alla volta. Questo si ottiene utilizzando la sincronizzazione.

Capire cos'è la sincronizzazione in Java

  • La sincronizzazione in Java si ottiene con l'aiuto della parola chiave "sincronizzato". Questa parola chiave può essere utilizzata per metodi o blocchi o oggetti ma non può essere utilizzata con classi e variabili. Un pezzo di codice sincronizzato consente a un solo thread di accedervi e modificarlo in un determinato momento.
  • Tuttavia, un pezzo di codice sincronizzato influisce sulle prestazioni del codice poiché aumenta il tempo di attesa di altri thread che tentano di accedervi. Quindi un pezzo di codice dovrebbe essere sincronizzato solo quando c'è la possibilità che si verifichi una condizione di competizione. Altrimenti si dovrebbe evitarlo.

Come funziona la sincronizzazione in Java internamente?

  • La sincronizzazione interna in Java è stata implementata con l'aiuto del concetto di blocco (noto anche come monitor). Ogni oggetto Java ha il suo blocco. In un blocco di codice sincronizzato, un thread deve acquisire il blocco prima di poter eseguire quel particolare blocco di codice. Una volta che un thread acquisisce il blocco, può eseguire quel pezzo di codice.
  • Al termine dell'esecuzione, rilascia automaticamente il blocco. Se un altro thread richiede di operare sul codice sincronizzato, attende che il thread corrente su di esso rilasci il blocco. Questo processo di acquisizione e rilascio di blocchi è gestito internamente dalla macchina virtuale Java. Un programma non è responsabile per l'acquisizione e il rilascio di blocchi da parte del thread. I thread rimanenti possono, tuttavia, eseguire simultaneamente qualsiasi altro pezzo di codice non sincronizzato.

Sincronizziamo il nostro esempio precedente sincronizzando il codice all'interno del metodo run utilizzando il blocco sincronizzato nella classe "Modifica" come di seguito:

Class Modify:
package JavaConcepts;
public class Modify implements Runnable(
private int myVar=0;
public int getMyVar() (
return myVar;
)
public void setMyVar(int myVar) (
this.myVar = myVar;
)
public void increment() (
myVar++;
)
@Override
public void run() (
// TODO Auto-generated method stub
synchronized(this) (
this.increment();
System.out.println("Current thread being executed "
+ Thread.currentThread().getName() + " Current Thread value " + this.getMyVar());
)
)
)

Il codice per la classe "RaceCondition" rimane lo stesso. Ora eseguendo il codice, l'output è il seguente:

output1:

Il thread corrente in esecuzione thread 1 Valore thread corrente 1

Il thread corrente in esecuzione thread 2 Valore thread corrente 2

Il thread corrente in esecuzione thread 3 Valore thread corrente 3

Output2:

Il thread corrente in esecuzione thread 1 Valore thread corrente 1

Il thread corrente in esecuzione thread 3 Valore thread corrente 2

Il thread corrente in esecuzione thread 2 Valore thread corrente 3

Si noti che il nostro codice fornisce l'output previsto. Qui ogni thread aumenta il valore di 1 per la variabile "myVar" (nella classe "Modifica").

Nota: la sincronizzazione è richiesta quando più thread operano sullo stesso oggetto. Se più thread funzionano su più oggetti, la sincronizzazione non è richiesta.

Ad esempio, modifichiamo il codice nella classe "RaceCondition" come di seguito e lavoriamo con la classe precedentemente non sincronizzata "Modifica".

package JavaConcepts;
public class RaceCondition (
public static void main(String() args) (
Modify mObj = new Modify();
Modify mObj1 = new Modify();
Modify mObj2 = new Modify();
Thread t1 = new Thread(mObj, "thread 1");
Thread t2 = new Thread(mObj1, "thread 2");
Thread t3 = new Thread(mObj2, "thread 3");
t1.start();
t2.start();
t3.start();
)
)

Produzione:

Il thread corrente in esecuzione thread 1 Valore thread corrente 1

Il thread corrente in esecuzione thread 2 Valore thread corrente 1

Il thread corrente in esecuzione thread 3 Valore thread corrente 1

Tipi di sincronizzazione in Java:

Esistono due tipi di sincronizzazione dei thread: uno si esclude a vicenda e l'altro la comunicazione tra thread.

1. Esclusivamente quotidiano

  • Metodo sincronizzato.
  • Metodo sincronizzato statico
  • Blocco sincronizzato.

2. Coordinamento del filo (comunicazione tra thread in java)

Si escludono a vicenda:

  • In questo caso, i thread ottengono il blocco prima di operare su un oggetto evitando così di lavorare con oggetti ai quali i loro valori sono stati manipolati da altri thread.
  • Ciò può essere ottenuto in tre modi:

io. Metodo sincronizzato: possiamo usare la parola chiave "sincronizzata" per un metodo, rendendolo così un metodo sincronizzato. Ogni thread che invoca il metodo sincronizzato otterrà il blocco per quell'oggetto e lo rilascerà una volta completata l'operazione. Nell'esempio sopra possiamo rendere il nostro metodo "run ()" sincronizzato usando la parola chiave "sincronizzata" dopo il modificatore di accesso.

@Override
public synchronized void run() (
// TODO Auto-generated method stub
this.increment();
System.out.println("Current thread being executed "
+ Thread.currentThread().getName() + " Current Thread value " + this.getMyVar());
)

L'output per questo caso sarà:

Il thread corrente in esecuzione thread 1 Valore thread corrente 1

Il thread corrente in esecuzione thread 3 Valore thread corrente 2

Il thread corrente in esecuzione thread 2 Valore thread corrente 3

ii. Metodo sincronizzato statico: per sincronizzare i metodi statici è necessario acquisire il blocco a livello di classe. Dopo che un thread ottiene solo il blocco a livello di classe, sarà in grado di eseguire un metodo statico. Mentre un thread contiene il blocco a livello di classe, nessun altro thread può eseguire nessun altro metodo sincronizzato statico di quella classe. Tuttavia, gli altri thread possono eseguire qualsiasi altro metodo regolare o metodo statico regolare o anche metodo sincronizzato non statico di quella classe.

Ad esempio, consideriamo la nostra classe "Modifica" e apportiamo modifiche convertendo il nostro metodo "incremento" in un metodo sincronizzato statico. Le modifiche al codice sono le seguenti:

package JavaConcepts;
public class Modify implements Runnable(
private static int myVar=0;
public int getMyVar() (
return myVar;
)
public void setMyVar(int myVar) (
this.myVar = myVar;
)
public static synchronized void increment() (
myVar++;
System.out.println("Current thread being executed " + Thread.currentThread().getName() + " Current Thread value " + myVar);
)
@Override
public void run() (
// TODO Auto-generated method stub
increment();
)
)

iii. Blocco sincronizzato: uno dei principali svantaggi del metodo sincronizzato è che aumenta i tempi di attesa dei thread influendo sulle prestazioni del codice. Pertanto, per poter sincronizzare solo le righe di codice richieste al posto dell'intero metodo, è necessario utilizzare un blocco sincronizzato. L'uso del blocco sincronizzato riduce il tempo di attesa dei thread e migliora anche le prestazioni. Nell'esempio precedente, abbiamo già utilizzato il blocco sincronizzato durante la sincronizzazione del nostro codice per la prima volta.

Esempio:
public void run() (
// TODO Auto-generated method stub
synchronized(this) (
this.increment();
System.out.println("Current thread being executed "
+ Thread.currentThread().getName() + " Current Thread value " + this.getMyVar());
)
)

Coordinamento discussione:

Per i thread sincronizzati, la comunicazione tra thread è un compito importante. I metodi integrati che aiutano a raggiungere la comunicazione tra thread per il codice sincronizzato sono in particolare:

  • aspettare()
  • notificare()
  • notifyAll ()

Nota: questi metodi appartengono alla classe oggetto e non alla classe thread. Affinché un thread sia in grado di invocare questi metodi su un oggetto, dovrebbe trattenere il blocco su quell'oggetto. Inoltre, questi metodi fanno sì che un thread rilasci il suo blocco sull'oggetto su cui viene invocato.

wait (): un thread che richiama il metodo wait (), rilascia il blocco sull'oggetto e passa allo stato di attesa. Ha due overload di metodo:

  • public final void wait () genera InterruptedException
  • attesa finale pubblico vuota (timeout lungo) genera InterruptedException
  • pubblico vuoto finale wait (long timeout, int nanos) genera InterruptedException

notify (): un thread invia un segnale a un altro thread nello stato di attesa facendo uso del metodo notify (). Invia la notifica a un solo thread in modo che questo thread possa riprenderne l'esecuzione. Quale thread riceverà la notifica tra tutti i thread nello stato di attesa dipende dalla Java Virtual Machine.

  • vuoto finale pubblico notification ()

notifyAll (): quando un thread richiama il metodo notifyAll (), viene notificato ogni thread nel suo stato di attesa. Questi thread verranno eseguiti uno dopo l'altro in base all'ordine deciso dalla Java Virtual Machine.

  • vuoto finale pubblico notificationAll ()

Conclusione

In questo articolo abbiamo visto come lavorare in un ambiente multi-thread può portare a un'incoerenza dei dati a causa di una condizione di competizione. In che modo la sincronizzazione ci aiuta a superare questo problema limitando un singolo thread a operare su una risorsa condivisa alla volta. Inoltre, come i thread sincronizzati comunicano tra loro.

Articoli consigliati:

Questa è stata una guida a Cos'è la sincronizzazione in Java ?. Qui discutiamo l'introduzione, la comprensione, le necessità, il funzionamento e i tipi di sincronizzazione con alcuni codici di esempio. Puoi anche consultare i nostri altri articoli suggeriti per saperne di più -

  1. Serializzazione in Java
  2. Che cos'è Generics in Java?
  3. Che cos'è l'API in Java?
  4. Che cos'è un albero binario in Java?
  5. Esempi e come funzionano i generici in C #

Categoria: