Sincronizzazione vs blocco


183

java.util.concurrentL'API fornisce una classe chiamata as Lock, che sostanzialmente serializzerebbe il controllo per accedere alla risorsa critica. Fornisce metodi come park()e unpark().

Possiamo fare cose simili se possiamo usare synchronizedparole chiave wait()e notify() notifyAll()metodi e utilizzo .

Mi chiedo quale di questi sia meglio in pratica e perché?


Risposte:


178

Se stai semplicemente bloccando un oggetto, preferirei utilizzarlo synchronized

Esempio:

Lock.acquire();
doSomethingNifty(); // Throws a NPE!
Lock.release(); // Oh noes, we never release the lock!

Devi fare esplicitamente try{} finally{}ovunque.

Considerando che con la sincronizzazione, è super chiaro e impossibile sbagliare:

synchronized(myObject) {
    doSomethingNifty();
}

Detto questo, Locks può essere più utile per cose più complicate in cui non è possibile acquisire e rilasciare in modo così pulito. Vorrei onestamente preferirei evitare di usare bare Locks in primo luogo, e semplicemente andare con un controllo della concorrenza più sofisticato come a CyclicBarriero a LinkedBlockingQueue, se soddisfano le tue esigenze.

Non ho mai avuto un motivo per usarlo wait()o notify()ma ci possono essere alcuni buoni.


1
Qual è la differenza tra wait / notification vs park / unpark di LockSupport? docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/…
Pacerier

6
All'inizio l'esempio aveva senso con i blocchi, ma poi mi sono reso conto che se si utilizza un tentativo, infine, si bloccherebbe quel problema con i blocchi non rilasciati
William Reed,

Ahh ... Uno di quei momenti per apprezzare il modello RAII in C ++. std::lock_guard
WhiZTiM,

67

Mi chiedo quale di questi sia meglio in pratica e perché?

Ho scoperto che Locke Condition(e altre nuove concurrentclassi) sono solo altri strumenti per la cassetta degli attrezzi. Potevo fare quasi tutto il necessario con il mio vecchio martello da carpentiere (la synchronizedparola chiave), ma era scomodo da usare in alcune situazioni. Molte di quelle situazioni imbarazzanti sono diventate molto più semplici una volta che ho aggiunto più strumenti alla mia cassetta degli attrezzi: un martello di gomma, un martello a sfera, un palanchino e alcuni punzoni per unghie. Tuttavia , il mio vecchio martello da carpentiere vede ancora la sua quota di utilizzo.

Non penso che uno sia davvero "migliore" dell'altro, ma piuttosto ognuno è più adatto a problemi diversi. In poche parole, il modello semplice e la natura orientata all'ambito synchronizedmi aiutano a proteggermi dai bug nel mio codice, ma quegli stessi vantaggi sono talvolta ostacoli in scenari più complessi. Sono questi scenari più complessi che il pacchetto simultaneo è stato creato per aiutare a risolvere. Ma l'utilizzo di questi costrutti di livello superiore richiede una gestione più esplicita e attenta del codice.

===

Penso che JavaDoc faccia un buon lavoro nel descrivere la distinzione tra Locke synchronized(l'enfasi è mia):

Le implementazioni di blocco forniscono operazioni di blocco più estese di quelle ottenibili utilizzando metodi e istruzioni sincronizzati. Consentono una strutturazione più flessibile , possono avere proprietà piuttosto diverse e possono supportare più oggetti Condizione associati .

...

L'uso di metodi o istruzioni sincronizzati fornisce l'accesso al blocco monitor implicito associato a ogni oggetto, ma impone che tutti i blocchi e le acquisizioni avvengano in modo strutturato a blocchi : quando vengono acquisiti più blocchi , devono essere rilasciati nell'ordine opposto , e tutti i blocchi devono essere rilasciati nello stesso ambito lessicale in cui sono stati acquisiti .

Mentre il meccanismo di scoping per metodi e istruzioni sincronizzati semplifica notevolmente la programmazione con i blocchi monitor e aiuta a evitare molti errori di programmazione comuni che coinvolgono i blocchi, ci sono occasioni in cui è necessario lavorare con i blocchi in modo più flessibile. Ad esempio, * * alcuni algoritmi * per attraversare strutture di dati accessibili contemporaneamente richiedono l'uso di "hand-over-hand" o "chain lock" : acquisisci il blocco del nodo A, quindi il nodo B, quindi rilascia A e acquisisci C, quindi rilasciare B e acquisire D e così via. Le implementazioni dell'interfaccia Lock consentono l'utilizzo di tali tecniche consentendo l'acquisizione e il rilascio di un lock in diversi ambiti econsentendo l'acquisizione e il rilascio di più blocchi in qualsiasi ordine .

Con questa maggiore flessibilità derivano ulteriori responsabilità . L' assenza di blocchi strutturati a blocchi rimuove il rilascio automatico dei blocchi che si verifica con metodi e istruzioni sincronizzati. Nella maggior parte dei casi, si dovrebbe usare il seguente linguaggio:

...

Quando si verificano il blocco e lo sblocco in diversi ambiti , è necessario assicurarsi che tutto il codice che viene eseguito mentre il blocco viene tenuto sia protetto da try-finally o try-catch per garantire che il blocco venga rilasciato quando necessario.

Le implementazioni di blocco forniscono funzionalità aggiuntive rispetto all'uso di metodi e istruzioni sincronizzati fornendo un tentativo non bloccante di acquisire un blocco (tryLock ()), un tentativo di acquisire il blocco che può essere interrotto (lockInterruptibly () e un tentativo di acquisizione il blocco che può timeout (tryLock (long, TimeUnit)).

...


23

È possibile ottenere tutto ciò che le utilità in java.util.concurrent fanno con le primitive di basso livello, come synchronized, volatileo di attesa / notificare

Tuttavia, la concorrenza è complicata e la maggior parte delle persone sbaglia almeno alcune parti, rendendo il loro codice errato o inefficiente (o entrambi).

L'API concorrente fornisce un approccio di livello superiore, che è più facile (e quindi più sicuro) da usare. In breve, non dovresti più utilizzare synchronized, volatile, wait, notifydirettamente.

Il blocco classe stessa si trova sul lato di livello più basso di questa cassetta degli attrezzi, si può anche non essere necessario utilizzare che, direttamente o (è possibile utilizzare Queuese semafori e roba del genere, ecc, la maggior parte del tempo).


2
La semplice vecchia attesa / notifica è considerata una primitiva di livello inferiore rispetto a java.util.concurrent.locks. Park / unpark di LockSupport o è il contrario?
Pacerier

@Pacerier: considero entrambi di basso livello (ovvero qualcosa che un programmatore di applicazioni vorrebbe evitare di utilizzare direttamente), ma certamente le parti di livello inferiore di java.util.concurrency (come il pacchetto locks) sono costruite in cima delle primitive JVM native in attesa / notifica (che sono anche di livello inferiore).
Thilo

2
No, intendo tra 3: Thread.sleep / interrupt, Object.wait / notification, LockSupport.park / unpark, qual è il più primitivo?
Pacerier

2
@Thilo Non sono sicuro di come sostenga la tua affermazione che java.util.concurrentè più facile [in generale] rispetto alle funzionalità del linguaggio ( synchronized, ecc ...). Quando usi java.util.concurrentdevi prendere l'abitudine di completare lock.lock(); try { ... } finally { lock.unlock() }prima di scrivere il codice mentre con un synchronizedstai praticamente bene dall'inizio. Solo su questa base direi che synchronizedè più facile (dato che vuoi il suo comportamento) di java.util.concurrent.locks.Lock. par 4
Iwan Aucamp,

1
Non pensare di poter replicare esattamente il comportamento delle classi AtomicXXX solo con le primitive di concorrenza, poiché si basano sull'invocazione CAS nativa non disponibile prima di java.util.concurrent.
Duncan Armstrong,

15

Ci sono 4 fattori principali nel motivo per cui vorresti usare synchronizedo java.util.concurrent.Lock.

Nota: il blocco sincronizzato è ciò che intendo quando dico blocco intrinseco.

  1. Quando Java 5 è uscito con ReentrantLocks, hanno dimostrato di avere una notevole differenza di velocità effettiva rispetto al blocco intrinseco. Se stai cercando un meccanismo di bloccaggio più veloce e stai utilizzando 1.5, prendi in considerazione jucReentrantLock. Il blocco intrinseco di Java 6 è ora comparabile.

  2. jucLock ha diversi meccanismi di blocco. Blocca interrompibile - tenta di bloccare fino a quando il filo di bloccaggio non viene interrotto; blocco temporizzato - tenta di bloccare per un certo periodo di tempo e rinuncia se non ci riesci; tryLock - tenta di bloccare, se qualche altro thread tiene il blocco rinunciare. Tutto questo è incluso a parte la semplice serratura. Il bloccaggio intrinseco offre solo un bloccaggio semplice

  3. Stile. Se sia 1 che 2 non rientrano nelle categorie di ciò che ti interessa della maggior parte delle persone, incluso me stesso, troverebbero la semenatica del blocco intrinseco più facile da leggere e meno dettagliata rispetto al blocco jucLock.
  4. Condizioni multiple. Un oggetto bloccato può essere notificato e atteso solo per un singolo caso. Il nuovo metodo LockCondition consente a un singolo Lock di avere diversi motivi per attendere o segnalare. Devo ancora davvero avere bisogno di questa funzionalità in pratica, ma è una bella caratteristica per chi ne ha bisogno.

Mi sono piaciuti i dettagli sul tuo commento. Vorrei aggiungere un altro punto elenco: ReadWriteLock fornisce un comportamento utile se si ha a che fare con più thread, solo alcuni dei quali devono scrivere sull'oggetto. Più thread possono leggere contemporaneamente l'oggetto e sono bloccati solo se un altro thread sta già scrivendo su di esso.
Sam Goldberg,

5

Vorrei aggiungere altre cose oltre alla risposta di Bert F.

Lockssupporta vari metodi per un controllo più preciso dei blocchi, che sono più espressivi dei monitor impliciti ( synchronizedblocchi)

Un blocco fornisce l'accesso esclusivo a una risorsa condivisa: solo un thread alla volta può acquisire il blocco e tutto l'accesso alla risorsa condivisa richiede che il blocco venga acquisito per primo. Tuttavia, alcuni blocchi potrebbero consentire l'accesso simultaneo a una risorsa condivisa, come il blocco di lettura di un ReadWriteLock.

Vantaggi del blocco sulla sincronizzazione dalla pagina della documentazione

  1. L'uso di metodi o istruzioni sincronizzati fornisce l'accesso al blocco del monitor implicito associato a ogni oggetto, ma forza l'acquisizione e il rilascio di tutti i blocchi in modo strutturato

  2. Le implementazioni di blocco forniscono funzionalità aggiuntive rispetto all'uso di metodi e istruzioni sincronizzati fornendo un tentativo non bloccante di acquisire un lock (tryLock()), un tentativo di acquisire il blocco che può essere interrotto ( lockInterruptibly()e un tentativo di acquisire il blocco che è possibile timeout (tryLock(long, TimeUnit)).

  3. Una classe Lock può anche fornire un comportamento e una semantica molto diversi da quelli del blocco implicito del monitor, come ordini garantiti, utilizzo non rientrante o rilevamento deadlock

ReentrantLock : in termini semplici secondo la mia comprensione, ReentrantLockconsente a un oggetto di rientrare da una sezione critica all'altra sezione critica. Poiché hai già il blocco per accedere a una sezione critica, puoi utilizzare un'altra sezione critica sullo stesso oggetto utilizzando il blocco corrente.

ReentrantLockcaratteristiche principali di questo articolo

  1. Possibilità di bloccare in modo interrotto.
  2. Possibilità di timeout durante l'attesa del blocco.
  3. Potere di creare un blocco equo.
  4. API per ottenere un elenco di thread in attesa di blocco.
  5. Flessibilità per provare a bloccare senza bloccare.

È possibile utilizzare ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLockper acquisire ulteriore controllo sul blocco granulare nelle operazioni di lettura e scrittura.

Oltre a questi tre ReentrantLock, java 8 fornisce un altro lucchetto

StampedLock:

Java 8 viene fornito con un nuovo tipo di blocco chiamato StampedLock che supporta anche i blocchi di lettura e scrittura proprio come nell'esempio sopra. Contrariamente a ReadWriteLock, i metodi di blocco di StampedLock restituiscono un timbro rappresentato da un valore lungo.

È possibile utilizzare questi timbri per rilasciare un blocco o per verificare se il blocco è ancora valido. Inoltre i blocchi stampati supportano un'altra modalità di blocco denominata blocco ottimistico.

Date un'occhiata a questo articolo su l'utilizzo di diversi tipi di ReentrantLocke StampedLockserrature.


4

La differenza principale è l'equità, in altre parole le richieste vengono gestite dalla FIFO o possono esserci delle barriere? La sincronizzazione a livello di metodo garantisce un'allocazione equa o FIFO del blocco. utilizzando

synchronized(foo) {
}

o

lock.acquire(); .....lock.release();

non assicura l'equità.

Se hai molte contese per il blocco, puoi facilmente imbatterti in un blocco in cui le richieste più recenti ottengono il blocco e le richieste più vecchie rimangono bloccate. Ho visto casi in cui 200 thread arrivano in breve tempo per un lucchetto e il 2 ° arrivato viene elaborato per ultimo. Questo va bene per alcune applicazioni, ma per altre è mortale.

Vedi il libro "Concorrenza in pratica" di Brian Goetz, sezione 13.3 per una discussione completa su questo argomento.


5
"La sincronizzazione a livello di metodo garantisce un'allocazione equa o FIFO del blocco." => Davvero? Stai dicendo che un metodo sincronizzato si comporta in modo diverso rispetto all'equità rispetto al contenuto del metodo in un blocco {} sincronizzato? Non la penso così, o ho capito male quella frase ...?
Weiresr,

Sì, anche se sorprendente e intuitivo è corretto. Il libro di Goetz è la migliore spiegazione.
Brian Tarbox,

Se guardi il codice fornito da @BrianTarbox, il blocco sincronizzato sta usando un oggetto diverso da "this" per bloccare. In teoria non c'è differenza tra un metodo sincronizzato e mettere l'intero corpo di detto metodo all'interno di un blocco sincronizzato, purché il blocco usi "questo" come blocco.
xburgos,

La risposta dovrebbe essere modificata per includere la citazione e chiarire che "garanzia" qui è "garanzia statistica", non deterministica.
Nathan Hughes,

Siamo spiacenti, ho appena scoperto che ho votato per errore questa risposta per errore qualche giorno fa (clic goffo). Sfortunatamente, SO attualmente non mi permette di ripristinarlo. Spero di poterlo riparare in seguito.
Mikko Östlund,

3

Il libro "Concorrenza in pratica" di Brian Goetz, sezione 13.3: "... Come ReentrantLock predefinito, il blocco intrinseco non offre garanzie di equità deterministica, ma le garanzie di equità statistica della maggior parte delle implementazioni di blocco sono abbastanza buone per quasi tutte le situazioni ..."


2

Lock semplifica la vita dei programmatori. Ecco alcune situazioni che possono essere raggiunte facilmente con il blocco.

  1. Blocca in un metodo e rilascia il blocco in un altro metodo.
  2. Se hai due thread che lavorano su due diversi pezzi di codice, tuttavia, nel primo thread ha un prerequisito per un certo pezzo di codice nel secondo thread (mentre alcuni altri thread lavorano anche sullo stesso pezzo di codice nel secondo filo contemporaneamente). Un blocco condiviso può risolvere questo problema abbastanza facilmente.
  3. Implementazione di monitor. Ad esempio, una semplice coda in cui i metodi put e get vengono eseguiti da molti altri thread. Tuttavia, non si desidera eseguire più metodi put (o get) contemporaneamente, né il metodo put e get in esecuzione contemporaneamente. Una serratura privata ti semplifica la vita.

Mentre, il blocco e le condizioni si basano sul meccanismo sincronizzato. Pertanto, può sicuramente essere in grado di ottenere la stessa funzionalità che è possibile ottenere utilizzando il blocco. Tuttavia, la risoluzione di scenari complessi con sincronizzazione può rendere la vita difficile e deviare dalla risoluzione del problema reale.


1

Differenza principale tra blocco e sincronizzazione:

  • con i blocchi, è possibile rilasciare e acquisire i blocchi in qualsiasi ordine.
  • con sincronizzato, è possibile rilasciare i blocchi solo nell'ordine in cui è stato acquisito.

0

Il blocco e la sincronizzazione del blocco hanno entrambi lo stesso scopo ma dipendono dall'uso. Considera la parte sotto

void randomFunction(){
.
.
.
synchronize(this){
//do some functionality
}

.
.
.
synchronize(this)
{
// do some functionality
}


} // end of randomFunction

Nel caso precedente, se un thread entra nel blocco di sincronizzazione, anche l'altro blocco viene bloccato. Se sono presenti più blocchi di sincronizzazione sullo stesso oggetto, tutti i blocchi vengono bloccati. In tali situazioni, java.util.concurrent.Lock può essere utilizzato per impedire il blocco indesiderato dei blocchi

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.