English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Introduzione dettagliata alla sincronizzazione dei thread in Java e codice di esempio

Sincronizzazione thread in Java

Sintesi:

Per accelerare l'esecuzione del codice, abbiamo adottato il metodo di multi-threading. L'esecuzione parallela rende il codice più efficiente, ma con essa si presentano problemi come molti thread che eseguono contemporaneamente nel programma, se cercano di modificare un oggetto contemporaneamente, potrebbe verificarsi un errore. In questo caso, è necessario utilizzare un meccanismo di sincronizzazione per gestire questi thread.

(1) Condizione di competizione

In un sistema operativo, una delle immagini che mi ha colpito è una che mostra dei processi, all'interno dei quali ci sono diversi thread, tutti questi thread puntano all'unisono alle risorse del processo. Java fa lo stesso, le risorse sono condivise tra i thread invece di avere una copia indipendente per ogni thread. In questo scenario di condivisione, è possibile che più thread accedano contemporaneamente a una risorsa, questo fenomeno è chiamato condizione di competizione.

In un sistema bancario, ogni thread gestisce un conto diverso, questi thread possono eseguire operazioni di trasferimento di denaro.
Quando un thread esegue un'operazione, per primo, mette il saldo del conto in un registro,第二步,它将寄存器中的数字减少要转出的钱数,第三步,它将结果写回余额中。
Il problema è che, quando questo thread ha completato i passaggi 1 e 2, un altro thread è stato svegliato e ha modificato il valore del saldo del primo thread, ma in questo momento il primo thread non è a conoscenza di questo. Dopo che il secondo thread ha completato il suo lavoro, il primo thread continua con il passo 3: scrivere il risultato nel saldo. In questo momento, cancella l'operazione del secondo thread, quindi il totale del sistema sicuramente subisce un errore.
Questo è uno scenario negativo di competizione in Java.

(II) Classe ReentrantLock

L'esempio sopra ci dice che se le nostre operazioni non sono atomiche, il blocco è sicuramente avvenuto, anche se a volte la probabilità è molto bassa, ma non possiamo escludere questa situazione. Non possiamo fare del nostro codice un'operazione atomica come nel sistema operativo, quello che possiamo fare è mettere in blocco il nostro codice per garantire la sicurezza. In un programma concorrente, se vogliamo accedere ai dati, dobbiamo mettere in blocco il nostro codice prima di farlo, e durante l'uso del blocco, le risorse coinvolte nel nostro codice sembrano essere 'bloccate', non possono essere accedute da altri thread fino a quando non apriamo il blocco.

In Java, la chiave synchronized e la classe ReentrantLock hanno entrambe la funzione di blocco. Iniziamo a discutere delle funzioni di ReentrantLock.

1. Costruttore di ReentrantLock

In questa classe, sono forniti due costruttori, uno è il costruttore di default, non c'è molto da dire, e uno è il costruttore con strategia fair. Questa strategia fair è molto più lenta rispetto alla chiave di blocco normale, e in alcuni casi non è veramente fair. E se non abbiamo motivi speciali per necessitare della strategia fair, è meglio non studiare questa strategia.

2. Acquisizione e rilascio

ReentrantLock myLock = new ReentrantLock();
//Creare l'oggetto
myLock.lock();
//Acquisire il blocco
try{
...
{}
finally{
myLock.unlock();
//Rilasciare il blocco
{}

Assicurati di rilasciare il blocco nel blocco finally! Abbiamo detto prima che gli errori non controllati possono causare la terminazione del thread. La terminazione misteriosa può fermare il programma dal continuare, e se non rilasci il blocco nel blocco finally, questo blocco non verrà mai rilasciato. Questo principio è lo stesso di quando usiamo .close() dopo un pacchetto nel nostro frame quotidiano. Parlando di close, devo dire che quando usiamo il blocco di sincronizzazione, non possiamo usare una 'try statement con risorse', perché questo blocco non viene chiuso con close. Se non sai cosa sono le try statement con risorse, non c'è bisogno di dire questa frase.

3. La reentrancy del blocco

Se stai utilizzando un programma ricorsivo o iterativo con un blocco di sincronizzazione, puoi usarlo con fiducia. La chiave di blocco ReentrantLock è reentrant, mantiene un contatore di chiamate che tiene traccia del numero di volte in cui è stato chiamato ogni volta che viene chiamato lock(), e deve essere rilasciato con unlock() dopo ogni chiamata a lock().

(三)Oggetto di condizione

Di solito, i thread scoprono un problema dopo aver acquisito il lock e entrato nella sezione critica, ossia che le risorse di cui hanno bisogno sono utilizzate da altri oggetti o non soddisfano le condizioni necessarie per eseguire. In questo caso, dobbiamo usare un oggetto di condizione per gestire i thread che hanno ottenuto un lock ma non possono fare nulla di utile.

if(a>b){
  a.set(b-1);
{}

1. Bloccato da sé

Questo è un esempio molto semplice di giudizio della condizione, ma non possiamo scrivere così nei programmi concorrenti. Il problema è che se un altro thread viene svegliato proprio dopo che questo thread ha fatto il giudizio, e l'altro thread modifica dopo aver operato in modo che a sia minore di b (la condizione dell'if non è più corretta).

A questo punto, potremmo pensare di mettere direttamente lo statement if all'interno del lock, assicurandoci che il nostro codice non venga interrotto. Ma c'è un altro problema: se l'if è falso, le istruzioni all'interno dell'if non vengono eseguite. Ma se dobbiamo eseguire le istruzioni all'interno dell'if, o dobbiamo aspettare che l'if diventi corretto prima di eseguire le istruzioni all'interno dell'if, improvvisamente scopriamo che l'if non diventerà mai corretto, perché il lock blocca questo thread e gli altri thread non possono accedere alla sezione critica e modificare i valori di a e b per rendere l'if corretto. Questo è molto imbarazzante, il nostro lock ci blocca e non possiamo uscire, mentre gli altri non possono entrare.

2. La classe Condition

Per risolvere questa situazione, usiamo il metodo newCondition della classe ReentrantLock per ottenere un oggetto di condizione.

Condition cd = myLock.newCondition();

Dopo aver ottenuto l'oggetto Condition, dobbiamo studiare quali metodi e funzioni ha questo oggetto. Prima di guardare l'API, torniamo al tema e scopriamo che il problema urgente da risolvere è il giudizio della condizione if, come possiamo:Quando si scopre che l'if è sbagliato dopo aver acquisito il lock, si dà l'opportunità agli altri thread e si aspetta che l'if diventi corretto di nuovo.

La classe Condition è stata creata per risolvere questo problema. Con la classe Condition, possiamo aggiungere direttamente il metodo await dopo lo statement if, che indica che il thread è bloccato e ha rilasciato il lock, aspettando che altri thread operino.

Attenzione, qui usiamo il termine bloccato, e abbiamo anche detto che il bloccato e l'attendere sono molto diversi: quando si cerca di ottenere un lock, una volta che il lock è libero, può ottenere automaticamente il lock, mentre quando si blocca per ottenere un lock, anche se c'è un lock libero, deve aspettare che il scheduler del thread gli permetta di mantenere il lock.

Altri thread eseguono correttamente il contenuto dello statement if e poi devono chiamare il metodo signalAll, che riattiverà tutti i thread bloccati a causa di questa condizione, permettendo loro di ottenere nuovamente l'opportunità. Questi thread sono autorizzati a continuare aIl punto di blocco continua.In questo momento, il thread dovrebbe testare di nuovo la condizione, se non può ancora soddisfare la condizione, è necessario ripetere l'operazione sopra menzionata.

ReentrantLock myLock = new ReentrantLock();
//Creare l'oggetto lock
myLock.lock();
//Bloccare la sezione critica sottostante
Condition cd = myLock.newCondition();
//Creare un oggetto di condizione, questo cd rappresenta l'oggetto di condizione
while(!(a>b))
  cd.await();
//Il ciclo while e la chiamata al metodo await sono la scrittura standard
//Se non si può soddisfare la condizione if, allora entrerà in uno stato di blocco, lasciando il lock e aspettando che altri lo attivino
a.set(b-1);
//Aspettare fino a quando non esce dal ciclo while, soddisfacenti i criteri di giudizio, eseguire la nostra funzione
cd.signalAll();
//Non dimenticare mai di chiamare il metodo signalall per attivare altri thread bloccati
//Se tutti i thread aspettano che altri thread signalall, entrano in uno stato di deadlock

Sarebbe molto brutto se tutti i thread aspettassero che altri thread signalall, entrando così in uno stato di deadlock. Lo stato di deadlock è una situazione in cui tutte le risorse necessarie per tutti i thread sono formate in un ciclo da altri thread, causando che nessuno possa eseguire. Infine, è necessario chiamare il metodo signalall per attivare altri thread bloccati per cd, per il nostro bene, per ridurre la possibilità di deadlock.

3.Sommario dell'oggetto Condition e del lock

In sintesi, l'oggetto Condition e il lock hanno alcune caratteristiche come segue.

  1. Il lock può essere utilizzato per proteggere segmenti di codice, in qualsiasi momento può entrare solo un thread nella regione protetta
  2. Il lock può gestire i thread che cercano di entrare nella sezione critica
  3. Il lock può avere una o più oggetti di condizione
  4. Ogni oggetto di condizione gestisce quei thread che non possono essere eseguiti per le ragioni descritte prima ma che sono già entrati nel segmento di codice protetto

(Quattro) Parola chiave synchronized

Abbiamo introdotto sopra ReentrantLock e l'oggetto Condition come un metodo per proteggere segmenti di codice, in Java esiste un altro meccanismo: utilizzare la parola chiave synchronized per修饰方法, aggiungendo così un lock interno al metodo. Dalla versione, ogni oggetto in Java ha un lock interno, ogni lock interno protegge i metodi che vengono modificati da synchronized. Questo significa che per chiamare questo metodo, è necessario ottenere il lock interno dell'oggetto.

1.Confronto tra synchronized e ReentrantLock

Ecco il codice che abbiamo preso sopra:

public void function(){
  ReentrantLock myLock = new ReentrantLock();
  myLock.lock();
  Condition cd = myLock.newCondition();
  while(!(a>b))
    cd.await();
  a.set(b-1);
  cd.signalAll();
{}

Se utilizziamo synchronized per implementare questo codice, diventerà così:

public synchronized void function(){
  while(!(a>b))
    wait();
  a.set(b-1);
  notifyAll();
{}

Cosa dobbiamo notare è che, quando si utilizza la parola chiave synchronized, non è necessario utilizzare oggetti ReentrantLock e Condition. Sostituiamo il metodo await con wait e il metodo signalAll con notifyAll. Questo rende il codice molto più semplice rispetto al passato.

2. Metodi statici synchronized

È lecito dichiarare metodi statici come synchronized. Se si chiama questo metodo, si ottiene il lock interno dell'oggetto della classe relativa. Ad esempio, se chiamiamo il metodo statico di una classe Test, il lock dell'oggetto Test.class viene bloccato.

3. Limitazioni dei lock interni e delle condizioni

Il lock interno è semplice, ma ha molti limiti:

  1. Non è possibile interrompere un thread che sta cercando di ottenere un lock
  2. Non è possibile impostare un timeout per ottenere un lock
  3. Non si può istanziare una condizione attraverso Condition. Ogni lock ha una singola condizione, che potrebbe non essere sufficiente

Quale dei due tipi di lock utilizzare nel codice? Lock e oggetti Condition o metodi sincroni? Nel libro Core Java ci sono alcune raccomandazioni:

  1. È meglio non utilizzare né ReentrantLock né la parola chiave synchronized. In molti casi puoi utilizzare il pacchetto java.util.concurrent
  2. Se synchronized soddisfa le tue esigenze di codice, utilizzalo preferibilmente
  3. Fino a quando non c'è un bisogno speciale di ReentrantLock, evitalo

Grazie per aver letto, spero di essere stato d'aiuto, grazie per il supporto al nostro sito!

Ti potrebbe interessare