Evitare sincronizzato (questo) in Java?


381

Ogni volta che viene visualizzata una domanda su SO sulla sincronizzazione Java, alcune persone sono molto ansiose di sottolineare che synchronized(this)dovrebbe essere evitato. Invece, sostengono, è preferibile un blocco su un riferimento privato.

Alcuni dei motivi indicati sono:

Altre persone, incluso me, sostengono che synchronized(this)è un linguaggio usato molto (anche nelle librerie Java), è sicuro e ben compreso. Non dovrebbe essere evitato perché hai un bug e non hai idea di cosa stia succedendo nel tuo programma multithread. In altre parole: se è applicabile, allora usalo.

Sono interessato a vedere alcuni esempi del mondo reale (niente roba da foobar) in cui thisè preferibile evitare un blocco quando synchronized(this)farebbe anche il lavoro.

Pertanto: dovresti sempre evitarlo synchronized(this)e sostituirlo con un lucchetto su un riferimento privato?


Alcune ulteriori informazioni (aggiornate man mano che vengono fornite le risposte):

  • stiamo parlando della sincronizzazione dell'istanza
  • vengono considerati sia il synchronizedmetodo implicito ( metodi) sia la forma esplicita disynchronized(this)
  • se citi Bloch o altre autorità sull'argomento, non tralasciare le parti che non ti piacciono (ad esempio Java efficace, oggetto su Sicurezza thread: in genere è il blocco sull'istanza stessa, ma ci sono eccezioni).
  • se hai bisogno di granularità nel tuo blocco diverso da quello synchronized(this)fornito, synchronized(this)non è applicabile, quindi non è questo il problema

4
Vorrei anche sottolineare che il contesto è importante: il bit "Tipicamente è il blocco sull'istanza stessa" è all'interno di una sezione relativa alla documentazione di una classe condizionale sicura per i thread, quando si rende pubblico il blocco. In altre parole, quella frase si applica quando hai già preso questa decisione.
Jon Skeet,

In assenza di sincronizzazione interna e quando è necessaria la sincronizzazione esterna, il blocco è spesso l'istanza stessa, dice sostanzialmente Bloch. Quindi perché questo non dovrebbe valere anche per la sincronizzazione interna con blocco su "questo"? (L'importanza della documentazione è un'altra questione.)
eljenso

Esiste un compromesso tra granularità estesa e cache aggiuntiva della CPU e sovraccarico di richieste di bus, poiché il blocco su un oggetto esterno richiederà molto probabilmente una modifica di una riga cache separata e lo scambio tra cache della CPU (cfr. MESIF e MOESI).
ArtemGr,

1
Penso che, nel mondo della programmazione difensiva, tu prevenga i bug non per idioma ma per codice. Quando qualcuno mi fa una domanda, "Quanto è ottimizzata la tua sincronizzazione?", Voglio dire "Molto" invece di "Molto, a meno che qualcun altro non segua il linguaggio".
Sgene9,

Risposte:


132

Tratterò ogni punto separatamente.

  1. Alcuni codici malvagi possono rubare il tuo lucchetto (molto popolare questo, ha anche una variante "accidentale")

    Sono più preoccupato per caso . Ciò che equivale a questo è che questo uso thisfa parte dell'interfaccia esposta della tua classe e dovrebbe essere documentata. A volte è richiesta la capacità di altri codici di utilizzare il blocco. Questo è vero per cose come Collections.synchronizedMap(vedi il javadoc).

  2. Tutti i metodi sincronizzati all'interno della stessa classe utilizzano lo stesso blocco esatto, riducendo la velocità effettiva

    Questo è un pensiero eccessivamente semplicistico; sbarazzarsi di synchronized(this)non risolverà il problema. La corretta sincronizzazione per la velocità effettiva richiederà più riflessione.

  3. Stai (inutilmente) esponendo troppe informazioni

    Questa è una variante di # 1. L'uso di synchronized(this)fa parte della tua interfaccia. Se non vuoi / hai bisogno di questo esposto, non farlo.


1. "sincronizzato" non fa parte dell'interfaccia esposta della tua classe. 2. d'accordo 3. vedi 1.
eljenso

66
Essenzialmente sincronizzato (questo) è esposto perché significa che il codice esterno può influire sul funzionamento della tua classe. Quindi asserisco che è necessario documentarlo come interfaccia, anche se la lingua non lo fa.
Darron,

15
Simile. Vedi Javadoc per Collections.synchronizedMap () - l'oggetto restituito utilizza sincronizzato (questo) internamente e si aspettano che il consumatore ne tragga vantaggio per utilizzare lo stesso blocco per operazioni atomiche su larga scala come l'iterazione.
Darron,

3
In effetti Collections.synchronizedMap () NON utilizza internamente sincronizzato (questo), utilizza un oggetto di blocco finale privato.
Bas Leijdekkers,

1
@Bas Leijdekkers: la documentazione specifica chiaramente che la sincronizzazione avviene sull'istanza della mappa restituita. La cosa interessante è che le visualizzazioni restituite keySet()e values()non si bloccano (loro) this, ma l'istanza della mappa, che è importante per ottenere un comportamento coerente per tutte le operazioni della mappa. Il motivo per cui l'oggetto lock viene scomposto in una variabile è che la sottoclasse ne ha SynchronizedSortedMapbisogno per l'implementazione di mappe secondarie che si bloccano sull'istanza della mappa originale.
Holger,

86

Bene, in primo luogo va sottolineato che:

public void blah() {
  synchronized (this) {
    // do stuff
  }
}

è semanticamente equivalente a:

public synchronized void blah() {
  // do stuff
}

quale è una ragione per non usare synchronized(this). Potresti sostenere che puoi fare cose intorno al synchronized(this)blocco. La solita ragione è cercare di evitare di dover fare il controllo sincronizzato, il che porta a tutti i tipi di problemi di concorrenza, in particolare il problema del doppio blocco controllato , che dimostra quanto sia difficile effettuare un controllo relativamente semplice threadsafe.

Una serratura privata è un meccanismo difensivo, che non è mai una cattiva idea.

Inoltre, come hai accennato, le serrature private possono controllare la granularità. Una serie di operazioni su un oggetto potrebbe non essere completamente correlata a un'altra, ma synchronized(this)escluderà reciprocamente l'accesso a tutte.

synchronized(this) proprio non ti dà niente.


4
"sincronizzato (questo) proprio non ti dà proprio niente." Ok, lo sostituisco con una sincronizzazione (myPrivateFinalLock). Cosa mi dà? Ne parli come un meccanismo difensivo. Da cosa sono protetto?
eljenso,

14
Sei protetto dal blocco accidentale (o dannoso) di "questo" da parte di oggetti esterni.
cletus,

14
Non sono affatto d'accordo con questa risposta: un blocco dovrebbe sempre essere tenuto per il minor tempo possibile, ed è proprio questo il motivo per cui vorresti "fare cose" attorno a un blocco sincronizzato invece di sincronizzare l'intero metodo .
Olivier,

5
Fare cose al di fuori del blocco sincronizzato è sempre ben intenzionato. Il punto è che le persone sbagliano molto spesso e non se ne rendono nemmeno conto, proprio come nel doppio problema di blocco. La strada per l'inferno è lastricata di buone intenzioni.
cletus,

16
In generale non sono d'accordo con "X è un meccanismo difensivo, che non è mai una cattiva idea". C'è un sacco di codice inutilmente gonfiato là fuori a causa di questo atteggiamento.
Finnw,

54

Durante l'utilizzo di sincronizzato (questo) si utilizza l'istanza di classe come un blocco stesso. Ciò significa che mentre il blocco viene acquisito dal thread 1 , il thread 2 dovrebbe attendere.

Supponiamo che il seguente codice:

public void method1() {
    // do something ...
    synchronized(this) {
        a ++;      
    }
    // ................
}


public void method2() {
    // do something ...
    synchronized(this) {
        b ++;      
    }
    // ................
}

Metodo 1 che modifica la variabile a e metodo 2 che modifica la variabile b , la modifica simultanea della stessa variabile da due thread dovrebbe essere evitata ed è così. MA mentre thread1 modifica a e thread2 modifica b può essere eseguito senza alcuna condizione di competizione.

Sfortunatamente, il codice sopra non lo consentirà poiché stiamo usando lo stesso riferimento per un blocco; Ciò significa che i thread, anche se non sono in condizioni di gara, dovrebbero attendere e ovviamente il codice sacrifica la concorrenza del programma.

La soluzione è utilizzare 2 diversi blocchi per due diverse variabili:

public class Test {

    private Object lockA = new Object();
    private Object lockB = new Object();

    public void method1() {
        // do something ...
        synchronized(lockA) {
            a ++;      
        }
        // ................
    }


    public void method2() {
        // do something ...
        synchronized(lockB) {
            b ++;      
        }
        // ................
    }

}

L'esempio sopra utilizza blocchi a grana più fine (2 blocchi invece di uno ( blocco A e blocco B per le variabili a e b rispettivamente) e di conseguenza permette una migliore concorrenza, dall'altro è diventato più complesso del primo esempio ...


Questo è molto pericoloso. È stato ora introdotto un requisito di ordinazione dei blocchi lato client (utente di questa classe). Se due thread chiamano method1 () e method2 () in un ordine diverso, è probabile che si blocchino, ma l'utente di questa classe non ha idea che sia così.
daveb,

7
La granularità non fornita da "sincronizzato (questo)" non rientra nell'ambito della mia domanda. E i tuoi campi di blocco non dovrebbero essere definitivi?
eljenso

10
per avere un deadlock dovremmo eseguire una chiamata dal blocco sincronizzato da A al blocco sincronizzato da B. daveb, ti sbagli ...
Andreas Bakurov

3
Non c'è deadlock in questo esempio per quanto posso vedere. Accetto che sia solo pseudocodice ma utilizzerei una delle implementazioni di java.util.concurrent.locks. Blocca come java.util.concurrent.locks.ReentrantLock
Shawn Vader

15

Mentre sono d'accordo sul non aderire ciecamente alle regole dogmatiche, lo scenario "rubare i furti" ti sembra così eccentrico? Un thread potrebbe effettivamente acquisire il blocco sull'oggetto "esternamente" ( synchronized(theObject) {...}), bloccando altri thread in attesa di metodi di istanza sincronizzati.

Se non credi nel codice dannoso, considera che questo codice potrebbe provenire da terze parti (ad esempio se sviluppi una sorta di server delle applicazioni).

La versione "accidentale" sembra meno probabile, ma come si suol dire, "fai qualcosa a prova di idiota e qualcuno inventerà un idiota migliore".

Quindi sono d'accordo con la scuola di pensiero che dipende dalla classe.


Modifica i seguenti 3 commenti di eljenso:

Non ho mai riscontrato il problema del furto di lucchetto, ma ecco uno scenario immaginario:

Supponiamo che il tuo sistema sia un servlet container e l'oggetto che stiamo prendendo in considerazione sia l' ServletContextimplementazione. Il suo getAttributemetodo deve essere thread-safe, poiché gli attributi di contesto sono dati condivisi; quindi lo dichiari come synchronized. Immaginiamo anche di fornire un servizio di hosting pubblico basato sull'implementazione del container.

Sono tuo cliente e distribuisco il mio servlet "buono" sul tuo sito. Succede che il mio codice contenga una chiamata agetAttribute .

Un hacker, travestito da un altro cliente, distribuisce il suo servlet dannoso sul tuo sito. Contiene il seguente codice ininit metodo:

synchronized (this.getServletConfig (). getServletContext ()) {
   while (true) {}
}

Supponendo che condividiamo lo stesso contesto servlet (consentito dalle specifiche fintanto che i due servlet sono sullo stesso host virtuale), la mia chiamata getAttributeè bloccata per sempre. L'hacker ha realizzato un DoS sul mio servlet.

Questo attacco non è possibile se getAttributeè sincronizzato su un blocco privato, poiché il codice di terze parti non può acquisire questo blocco.

Devo ammettere che l'esempio è inventato e una visione troppo semplicistica di come funziona un contenitore servlet, ma IMHO ne dimostra il punto.

Quindi farei la mia scelta progettuale in base alla considerazione della sicurezza: avrò il controllo completo sul codice che ha accesso alle istanze? Quale sarebbe la conseguenza di un thread che tiene un lucchetto su un'istanza indefinitamente?


dipende da cosa fa la classe: se si tratta di un oggetto "importante", bloccare il riferimento privato? Altrimenti sarà sufficiente il blocco dell'istanza?
eljenso,

6
Sì, lo scenario del furto di lucchetto mi sembra molto inverosimile. Tutti lo menzionano, ma chi lo ha effettivamente fatto o provato? Se "accidentalmente" blocchi un oggetto che non dovresti, allora c'è un nome per questo tipo di situazione: è un bug. Aggiustalo.
eljenso

4
Inoltre, il blocco dei riferimenti interni non è libero dall'attacco di sincronizzazione esterno: se sai che una certa parte sincronizzata del codice attende che si verifichi un evento esterno (ad es. Scrittura di file, valore nel DB, evento timer) probabilmente provvedere anche al blocco.
eljenso,

Lasciami confessare che sono uno di quegli idioti, anche se l'ho fatto quando ero giovane. Ho pensato che il codice fosse più pulito non creando un oggetto lock esplicito e, invece, ho usato un altro oggetto finale privato che doveva partecipare al monitor. Non sapevo che l'oggetto stesso si sincronizzasse su se stesso. Puoi immaginare il conseguente dirottamento ...
Alan Cabrera,

12

Dipende dalla situazione.
Se esiste solo un'entità di condivisione o più di una.

Vedi l'esempio di lavoro completo qui

Una piccola introduzione

Thread ed entità condivisibili
È possibile che più thread accedano alla stessa entità, ad es. Connessioni multipleTread che condividono un singolo messaggioQueue. Poiché i thread vengono eseguiti contemporaneamente, potrebbe esserci la possibilità di sovrascrivere i propri dati da un altro, il che potrebbe essere una situazione incasinata.
Quindi abbiamo bisogno di un modo per garantire che l'entità condivisibile sia accessibile solo da un thread alla volta. (CONCORRENZA).

Blocco
sincronizzato Il blocco sincronizzato () è un modo per garantire l'accesso simultaneo all'entità condivisibile.
In primo luogo, una piccola analogia
Supponiamo che ci siano due persone P1, P2 (discussioni) un lavabo (entità condivisibile) all'interno di un bagno e c'è una porta (serratura).
Ora vogliamo che una persona usi il lavandino alla volta.
Un approccio è quello di bloccare la porta con P1 quando la porta è bloccata P2 attende fino a quando p1 completa il suo lavoro
P1 sblocca la porta
quindi solo p1 può usare il lavandino.

sintassi.

synchronized(this)
{
  SHARED_ENTITY.....
}

"this" ha fornito il blocco intrinseco associato alla classe (lo sviluppatore Java ha progettato la classe Object in modo tale che ogni oggetto possa funzionare come monitor). L'approccio sopra funziona bene quando ci sono solo un'entità condivisa e più thread (1: N). N entità condivisibili-thread M Ora pensa a una situazione in cui ci sono due lavandini all'interno di un bagno e una sola porta. Se stiamo usando l'approccio precedente, solo p1 può usare un lavandino alla volta mentre p2 attenderà all'esterno. È uno spreco di risorse poiché nessuno usa B2 (lavabo). Un approccio più saggio sarebbe quello di creare una stanza più piccola all'interno del bagno e fornire loro una porta per lavandino. In questo modo, P1 può accedere a B1 e P2 può accedere a B2 e viceversa.
inserisci qui la descrizione dell'immagine

washbasin1;  
washbasin2;

Object lock1=new Object();
Object lock2=new Object();

  synchronized(lock1)
  {
    washbasin1;
  }

  synchronized(lock2)
  {
    washbasin2;
  }

inserisci qui la descrizione dell'immagine
inserisci qui la descrizione dell'immagine

Vedi di più su Discussioni ----> qui


11

Sembra esserci un diverso consenso nei campi C # e Java su questo. La maggior parte del codice Java che ho visto utilizza:

// apply mutex to this instance
synchronized(this) {
    // do work here
}

mentre la maggior parte del codice C # opta probabilmente per il più sicuro:

// instance level lock object
private readonly object _syncObj = new object();

...

// apply mutex to private instance level field (a System.Object usually)
lock(_syncObj)
{
    // do work here
}

Il linguaggio C # è sicuramente più sicuro. Come accennato in precedenza, nessun accesso dannoso / accidentale al blocco può essere effettuato dall'esterno dell'istanza. Anche il codice Java ha questo rischio, ma sembra che la comunità Java abbia gravitato nel tempo verso la versione leggermente meno sicura, ma leggermente più concisa.

Questo non è inteso come uno scavo contro Java, ma solo un riflesso della mia esperienza di lavoro in entrambe le lingue.


3
Forse dato che C # è una lingua più giovane, hanno imparato da cattivi schemi che sono stati individuati nel campo di Java e hanno codificato cose del genere meglio? Ci sono anche meno single? :)
Bill K,

3
Lui lui. Abbastanza probabilmente vero, ma non ho intenzione di salire sull'esca! Una cosa che posso dire con certezza è che ci sono più lettere maiuscole nel codice C #;)
serg10

1
Semplicemente non vero (per dirla bene)
martedì

7

Il java.util.concurrentpacchetto ha notevolmente ridotto la complessità del mio codice thread-safe. Ho solo prove aneddotiche da continuare, ma la maggior parte del lavoro che ho visto synchronized(x)sembra reimplementare un Lock, un semaforo o un Latch, ma usando i monitor di livello inferiore.

Con questo in mente, la sincronizzazione usando uno di questi meccanismi è analoga alla sincronizzazione su un oggetto interno, piuttosto che perdere una serratura. Ciò è utile in quanto hai la certezza assoluta di controllare l'ingresso nel monitor da due o più thread.


6
  1. Rendi immutabili i tuoi dati se possibile ( finalvariabili)
  2. Se non puoi evitare la mutazione dei dati condivisi su più thread, utilizza costrutti di programmazione di alto livello [ad es. LockAPI granulare ]

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 sia acquisito per primo.

Codice di esempio da utilizzare ReentrantLockche implementa l' Lockinterfaccia

 class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...

   public void m() {
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }

Vantaggi di Lock over Synchronized (questo)

  1. L'uso di metodi o istruzioni sincronizzati forza l'acquisizione e il rilascio di tutti i blocchi in modo strutturato a blocchi.

  2. Le implementazioni di blocco forniscono funzionalità aggiuntive rispetto all'uso di metodi e istruzioni sincronizzati fornendo

    1. Un tentativo non bloccante di acquisire un blocco (tryLock() )
    2. Un tentativo di acquisire il blocco che può essere interrotto (lockInterruptibly() )
    3. Un tentativo di acquisire il blocco che può timeout ( tryLock(long, TimeUnit)).
  3. Una classe Lock può anche fornire comportamenti e semantica molto diversi da quelli del blocco monitor implicito, come ad esempio

    1. ordinazione garantita
    2. utilizzo non concorrente
    3. Rilevamento deadlock

Dai un'occhiata a questa domanda SE riguardante vari tipi di Locks:

Sincronizzazione vs blocco

Puoi ottenere la sicurezza dei thread utilizzando l'API di concorrenza avanzata anziché i blocchi sincronizzati. Questa pagina di documentazione fornisce buoni costrutti di programmazione per ottenere la sicurezza del thread.

Lock Objects supporta i modi di bloccare che semplificano molte applicazioni simultanee.

I dirigenti definiscono un'API di alto livello per l'avvio e la gestione dei thread. Le implementazioni di Executor fornite da java.util.concurrent forniscono la gestione del pool di thread adatta per applicazioni su larga scala.

Le raccolte simultanee semplificano la gestione di grandi raccolte di dati e possono ridurre notevolmente la necessità di sincronizzazione.

Le variabili atomiche hanno funzionalità che riducono al minimo la sincronizzazione e aiutano a evitare errori di coerenza della memoria.

ThreadLocalRandom (in JDK 7) fornisce una generazione efficiente di numeri pseudocasuali da più thread.

Consultare anche i pacchetti java.util.concurrent e java.util.concurrent.atomic per altri costrutti di programmazione.


5

Se hai deciso che:

  • la cosa che devi fare è bloccare l'oggetto corrente; e
  • vuoi bloccarlo con una granularità minore di un intero metodo;

quindi non vedo il tabù su synchronizezd (questo).

Alcune persone usano deliberatamente sincronizzato (questo) (invece di contrassegnare il metodo sincronizzato) all'interno dell'intero contenuto di un metodo perché pensano che sia "più chiaro per il lettore" su quale oggetto viene effettivamente sincronizzato. Finché le persone stanno facendo una scelta informata (ad esempio, capendo che facendo ciò, in realtà stanno inserendo bytecode extra nel metodo e questo potrebbe avere un effetto a catena su potenziali ottimizzazioni), non vedo particolarmente un problema con questo . Dovresti sempre documentare il comportamento concorrente del tuo programma, quindi non vedo l'argomento "sincronizzato" pubblica il comportamento "come così convincente.

Per quanto riguarda la domanda su quale blocco dell'oggetto dovresti usare, penso che non ci sia nulla di sbagliato nella sincronizzazione dell'oggetto corrente se ciò fosse previsto dalla logica di ciò che stai facendo e di come la tua classe verrebbe normalmente usata . Ad esempio, con una raccolta, l'oggetto che ti aspetteresti logicamente di bloccare è generalmente la raccolta stessa.


1
"se questo sarebbe prevedibile dalla logica ..." è un punto che sto cercando di superare. Non vedo il punto di usare sempre le serrature private, anche se il consenso generale sembra essere che è meglio, dal momento che non fa male ed è più difensivo.
eljenso,

4

Penso che ci sia una buona spiegazione sul perché ognuna di queste siano tecniche vitali sotto il tuo controllo in un libro intitolato Java Concurrency In Practice di Brian Goetz. Rende molto chiaro un punto: è necessario utilizzare lo stesso blocco "OVUNQUE" per proteggere lo stato dell'oggetto. Il metodo sincronizzato e la sincronizzazione su un oggetto spesso vanno di pari passo. Ad esempio Vector sincronizza tutti i suoi metodi. Se hai un handle per un oggetto vettoriale e hai intenzione di "mettere se assente", allora semplicemente sincronizzare Vector i suoi metodi individuali non ti proteggerà dalla corruzione dello stato. Devi sincronizzare usando sincronizzato (vectorHandle). Ciò comporterà l'acquisizione del SAME lock da parte di ogni thread che ha un handle per il vettore e proteggerà lo stato generale del vettore. Questo si chiama blocco lato client. Sappiamo infatti che il vettore sincronizza (questo) / sincronizza tutti i suoi metodi e quindi la sincronizzazione sull'oggetto vectorHandle comporterà una corretta sincronizzazione dello stato degli oggetti vettoriali. È sciocco credere di essere thread-safe solo perché si utilizza una raccolta thread-safe. Questo è precisamente il motivo per cui ConcurrentHashMap ha introdotto esplicitamente il metodo putIfAbsent per rendere tali operazioni atomiche.

In sintesi

  1. La sincronizzazione a livello di metodo consente il blocco lato client.
  2. Se si dispone di un oggetto di blocco privato, ciò rende impossibile il blocco lato client. Questo va bene se sai che la tua classe non ha un tipo di funzionalità "put if absent".
  3. Se stai progettando una libreria - quindi la sincronizzazione su questo o la sincronizzazione del metodo è spesso più saggia. Perché raramente sei in grado di decidere come verrà utilizzata la tua classe.
  4. Se Vector avesse usato un oggetto di blocco privato - sarebbe stato impossibile ottenere "messo se assente" giusto. Il codice client non otterrà mai un handle per il blocco privato, infrangendo così la regola fondamentale dell'utilizzo di EXACT SAME LOCK per proteggere il suo stato.
  5. La sincronizzazione su questo o sui metodi sincronizzati ha un problema, come altri hanno sottolineato: qualcuno potrebbe ottenere un blocco e non rilasciarlo mai. Tutti gli altri thread continuerebbero ad attendere il rilascio del blocco.
  6. Quindi sappi cosa stai facendo e adotta quello che è corretto.
  7. Qualcuno ha sostenuto che avere un oggetto di blocco privato offre una granularità migliore, ad esempio se due operazioni non sono correlate, potrebbero essere protette da blocchi diversi con conseguente migliore produttività. Ma penso che questo sia odore di design e non odore di codice - se due operazioni sono completamente estranee perché fanno parte della classe SAME? Perché un club di classe non ha affatto funzionalità indipendenti? Può essere una classe di utilità? Hmmmm - alcuni util che forniscono la manipolazione di stringhe e la formattazione della data del calendario attraverso la stessa istanza ?? ... non ha alcun senso per me almeno !!

3

No, non dovresti sempre . Tuttavia, tendo ad evitarlo quando ci sono più preoccupazioni su un oggetto particolare che devono essere solo sicure rispetto a se stesse. Ad esempio, potresti avere un oggetto dati mutabile con campi "etichetta" e "padre"; questi devono essere sicuri per i thread, ma cambiarne uno non è necessario bloccare l'altro da scrivere / leggere. (In pratica lo eviterei dichiarando i campi volatili e / o usando i wrapper AtomicFoo di java.util.concurrent).

La sincronizzazione in generale è un po 'goffa, dato che schiaccia un grosso blocco anziché pensare esattamente a come i thread potrebbero essere fatti funzionare l'uno attorno all'altro. L'uso synchronized(this)è ancora più maldestro e antisociale, poiché dice "nessuno può cambiare nulla in questa classe mentre io tengo il lucchetto". Quanto spesso devi farlo?

Preferirei di gran lunga avere serrature più granulari; anche se vuoi impedire che tutto cambi (forse stai serializzando l'oggetto), puoi semplicemente acquisire tutti i blocchi per ottenere la stessa cosa, inoltre è più esplicito in quel modo. Quando lo usi synchronized(this), non è chiaro esattamente perché ti stai sincronizzando o quali potrebbero essere gli effetti collaterali. Se usi synchronized(labelMonitor), o ancora meglio labelLock.getWriteLock().lock(), è chiaro cosa stai facendo e quali sono gli effetti della tua sezione critica.


3

Risposta breve : devi capire la differenza e scegliere a seconda del codice.

Risposta lunga : In generale, preferirei evitare di sincronizzare (questo) per ridurre la contesa, ma i blocchi privati ​​aggiungono complessità di cui devi essere consapevole. Quindi usa la sincronizzazione corretta per il lavoro giusto. Se non hai esperienza con la programmazione multi-thread, preferirei attenermi al blocco dell'istanza e leggere su questo argomento. (Detto questo: il solo uso di synchronize (this) non rende automaticamente la tua classe completamente thread-safe.) Questo non è un argomento facile ma una volta che ti ci abitui, la risposta se usare la sincronizzazione (this) o no viene naturale .


Ti capisco correttamente quando dici che dipende dalla tua esperienza?
eljenso

Innanzitutto dipende dal codice che si desidera scrivere. Sto solo dicendo che potresti aver bisogno di un po 'più di esperienza quando ti trasferisci per non usare la sincronizzazione (questo).
martedì

2

Un lucchetto viene utilizzato per la visibilità o per proteggere alcuni dati da modifiche simultanee che possono portare alla competizione.

Quando devi solo eseguire operazioni di tipo primitivo per essere atomiche, ci sono opzioni disponibili come AtomicIntegere simili.

Ma supponiamo di avere due numeri interi collegati tra loro simili xe ycoordinate, che sono correlati tra loro e che dovrebbero essere modificati in modo atomico. Quindi li proteggeresti usando una stessa serratura.

Un blocco dovrebbe proteggere solo lo stato correlato tra loro. Niente di meno e niente di più. Se si utilizza synchronized(this)in ciascun metodo, anche se lo stato della classe non è correlato, tutti i thread dovranno affrontare la contesa anche se si aggiorna lo stato non correlato.

class Point{
   private int x;
   private int y;

   public Point(int x, int y){
       this.x = x;
       this.y = y;
   }

   //mutating methods should be guarded by same lock
   public synchronized void changeCoordinates(int x, int y){
       this.x = x;
       this.y = y;
   }
}

Nell'esempio sopra ho un solo metodo che muta entrambi xe ynon due metodi diversi come xe ysono correlati e se avessi dato due metodi diversi per la mutazione xe yseparatamente, non sarebbe stato sicuro per il thread.

Questo esempio è solo per dimostrare e non necessariamente il modo in cui dovrebbe essere implementato. Il modo migliore per farlo sarebbe renderlo IMMUTABILE .

Ora in opposizione Pointall'esempio, c'è un esempio di TwoCountersgià fornito da @Andreas in cui lo stato che è protetto da due diversi blocchi in quanto lo stato non è correlato tra loro.

Il processo di utilizzo di diversi blocchi per proteggere stati non correlati è chiamato Lock Striping o Lock Splitting


1

Il motivo per non sincronizzarsi su questo è che a volte è necessario più di un blocco (il secondo blocco viene spesso rimosso dopo qualche riflessione aggiuntiva, ma è ancora necessario nello stato intermedio). Se si blocca su questo , si deve sempre ricordare che una delle due serrature è questo ; se blocchi un oggetto privato, il nome della variabile ti dice questo.

Dal punto di vista del lettore, se vedi che si blocca su questo , devi sempre rispondere alle due domande:

  1. che tipo di accesso è protetto da questo ?
  2. una serratura è davvero abbastanza, qualcuno non ha introdotto un bug?

Un esempio:

class BadObject {
    private Something mStuff;
    synchronized setStuff(Something stuff) {
        mStuff = stuff;
    }
    synchronized getStuff(Something stuff) {
        return mStuff;
    }
    private MyListener myListener = new MyListener() {
        public void onMyEvent(...) {
            setStuff(...);
        }
    }
    synchronized void longOperation(MyListener l) {
        ...
        l.onMyEvent(...);
        ...
    }
}

Se due thread iniziano longOperation()su due diverse istanze di BadObject, acquisiscono i loro blocchi; quando è il momento di invocare l.onMyEvent(...), abbiamo un deadlock perché nessuno dei thread può acquisire il blocco dell'altro oggetto.

In questo esempio possiamo eliminare il deadlock usando due blocchi, uno per le operazioni brevi e uno per quelli lunghi.


2
L'unico modo per ottenere un deadlock in questo esempio è quando BadObjectA invoca longOperationB, passando A myListenere viceversa. Non impossibile, ma piuttosto contorto, a sostegno dei miei punti precedenti.
eljenso,

1

Come già detto qui, il blocco sincronizzato può utilizzare la variabile definita dall'utente come oggetto di blocco, quando la funzione sincronizzata utilizza solo "questo". E ovviamente puoi manipolare aree della tua funzione che dovrebbero essere sincronizzate e così via.

Ma tutti dicono che nessuna differenza tra funzione sincronizzata e blocco che copre l'intera funzione usando "questo" come oggetto di blocco. Ciò non è vero, la differenza è nel codice byte che verrà generato in entrambe le situazioni. In caso di utilizzo del blocco sincronizzato dovrebbe essere allocata la variabile locale che contiene il riferimento a "this". E di conseguenza avremo dimensioni della funzione un po 'più grandi (non rilevanti se hai solo un numero limitato di funzioni).

Una spiegazione più dettagliata della differenza è possibile trovare qui: http://www.artima.com/insidejvm/ed2/threadsynchP.html

Anche l'uso del blocco sincronizzato non è buono a causa del seguente punto di vista:

La parola chiave sincronizzata è molto limitata in un'area: quando si esce da un blocco sincronizzato, tutti i thread in attesa di quel blocco devono essere sbloccati, ma solo uno di questi thread riesce a prendere il blocco; tutti gli altri vedono che il blocco è stato preso e tornano allo stato bloccato. Non sono solo molti cicli di elaborazione sprecati: spesso il cambio di contesto per sbloccare un thread comporta anche il paging della memoria dal disco, ed è molto, molto, costoso.

Per maggiori dettagli in questo settore, ti consiglio di leggere questo articolo: http://java.dzone.com/articles/synchronized-considered


1

Questo è davvero solo supplementare alle altre risposte, ma se la tua principale obiezione all'utilizzo di oggetti privati ​​per il blocco è che ingombra la tua classe con campi che non sono correlati alla logica aziendale, Project Lombok deve @Synchronizedgenerare la piastra di caldaia in fase di compilazione:

@Synchronized
public int foo() {
    return 0;
}

compila a

private final Object $lock = new Object[0];

public int foo() {
    synchronized($lock) {
        return 0;
    }
}

0

Un buon esempio per l'uso sincronizzato (questo).

// add listener
public final synchronized void addListener(IListener l) {listeners.add(l);}
// remove listener
public final synchronized void removeListener(IListener l) {listeners.remove(l);}
// routine that raise events
public void run() {
   // some code here...
   Set ls;
   synchronized(this) {
      ls = listeners.clone();
   }
   for (IListener l : ls) { l.processEvent(event); }
   // some code here...
}

Come puoi vedere qui, usiamo la sincronizzazione su questo per cooperare facilmente lungo (possibilmente infinito ciclo di metodo di esecuzione) con alcuni metodi sincronizzati lì.

Naturalmente può essere facilmente riscritto usando sincronizzato su campo privato. Ma a volte, quando abbiamo già qualche progetto con metodi sincronizzati (ad es. Classe legacy, da cui deriviamo, sincronizzato (questo) può essere l'unica soluzione).


Qui è possibile utilizzare qualsiasi oggetto come blocco. Non è necessario this. Potrebbe essere un campo privato.
Finnw,

Corretto, ma lo scopo di questo esempio era mostrare come eseguire la sincronizzazione corretta, se decidessimo di utilizzare la sincronizzazione del metodo.
Bart Prokop,

0

Dipende dal compito che vuoi fare, ma non lo userei. Inoltre, controlla se il salvataggio del thread che vuoi accompagnare non può essere fatto sincronizzando (questo) in primo luogo? Ci sono anche alcuni bei blocchi nell'API che potrebbero aiutarti :)


0

Voglio solo menzionare una possibile soluzione per riferimenti privati ​​univoci in parti atomiche di codice senza dipendenze. È possibile utilizzare una hashmap statica con blocchi e un semplice metodo statico denominato atomic () che crea automaticamente i riferimenti richiesti utilizzando le informazioni sullo stack (nome completo della classe e numero di riga). Quindi è possibile utilizzare questo metodo per sincronizzare le istruzioni senza scrivere un nuovo oggetto di blocco.

// Synchronization objects (locks)
private static HashMap<String, Object> locks = new HashMap<String, Object>();
// Simple method
private static Object atomic() {
    StackTraceElement [] stack = Thread.currentThread().getStackTrace(); // get execution point 
    StackTraceElement exepoint = stack[2];
    // creates unique key from class name and line number using execution point
    String key = String.format("%s#%d", exepoint.getClassName(), exepoint.getLineNumber()); 
    Object lock = locks.get(key); // use old or create new lock
    if (lock == null) {
        lock = new Object();
        locks.put(key, lock);
    }
    return lock; // return reference to lock
}
// Synchronized code
void dosomething1() {
    // start commands
    synchronized (atomic()) {
        // atomic commands 1
        ...
    }
    // other command
}
// Synchronized code
void dosomething2() {
    // start commands
    synchronized (atomic()) {
        // atomic commands 2
        ...
    }
    // other command
}

0

Evita l'uso synchronized(this)come meccanismo di blocco: questo blocca l'intera istanza della classe e può causare deadlock. In questi casi, refactoring il codice per bloccare solo un metodo o una variabile specifici, in questo modo l'intera classe non viene bloccata. Synchronisedpuò essere utilizzato a livello di metodo.
Invece di usare synchronized(this), sotto il codice mostra come si potrebbe semplicemente bloccare un metodo.

   public void foo() {
if(operation = null) {
    synchronized(foo) { 
if (operation == null) {
 // enter your code that this method has to handle...
          }
        }
      }
    }

0

I miei due centesimi nel 2019 anche se questa domanda avrebbe potuto essere risolta già.

Bloccare "this" non è male se sai cosa stai facendo, ma dietro la scena è bloccarsi su "this" (che sfortunatamente ciò che consente la parola chiave sincronizzata nella definizione del metodo).

Se vuoi davvero che gli utenti della tua classe siano in grado di "rubare" il tuo blocco (cioè impedire ad altri thread di gestirlo), vuoi davvero che tutti i metodi sincronizzati attendano mentre è in esecuzione un altro metodo di sincronizzazione e così via. Dovrebbe essere intenzionale e ben ponderato (e quindi documentato per aiutare i tuoi utenti a capirlo).

Per approfondire, al contrario devi sapere cosa stai "guadagnando" (o "perdere") se blocchi un blocco non accessibile (nessuno può "rubare" il tuo blocco, hai il controllo totale e così via. ..).

Il problema per me è che la parola chiave sincronizzata nella firma della definizione del metodo rende troppo facile per i programmatori non pensare a cosa bloccare, che è una cosa molto importante a cui pensare se non si desidera incorrere in problemi in un multi -threaded program.

Non si può sostenere che "in genere" non si desidera che gli utenti della propria classe siano in grado di fare queste cose o che "in genere" si desideri ... Dipende da quale funzionalità si sta codificando. Non è possibile stabilire una regola del pollice in quanto non è possibile prevedere tutti i casi d'uso.

Si consideri, ad esempio, il printwriter che utilizza un blocco interno ma che le persone fanno fatica a usarlo da più thread se non vogliono che il loro output si interfogli.

Se il tuo lucchetto è accessibile al di fuori della classe o no, la tua decisione di programmatore dipende dalla funzionalità della classe. Fa parte dell'API. Ad esempio, non è possibile passare da sincronizzato (questo) a sincronizzato (provateObjet) senza rischiare di interrompere le modifiche nel codice che lo utilizzano.

Nota 1: So che puoi ottenere tutto ciò che "sincronizza" (")" utilizzando un oggetto di blocco esplicito ed esponendolo, ma penso che non sia necessario se il tuo comportamento è ben documentato e sai effettivamente cosa significa bloccare su "questo".

Nota 2: Non concordo con l'argomento secondo cui se un codice ruba accidentalmente il tuo lucchetto è un bug e devi risolverlo. Questo in un certo senso è lo stesso argomento del dire che posso rendere pubblici tutti i miei metodi anche se non sono fatti per essere pubblici. Se qualcuno sta 'accidentalmente' chiamando il mio inteso come metodo privato è un bug. Perché abilitare questo incidente in primo luogo !!! Se la capacità di rubare il lucchetto è un problema per la tua classe, non permetterlo. Così semplice.


-3

Penso che i punti uno (qualcun altro che usa il tuo lucchetto) e due (tutti i metodi che usano lo stesso lucchetto inutilmente) possano accadere in qualsiasi applicazione abbastanza grande. Soprattutto quando non c'è una buona comunicazione tra gli sviluppatori.

Non è una pietra miliare, è principalmente un problema di buone pratiche e di prevenzione degli errori.

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.