Ghid pentru cuvântul cheie sincronizat în Java

1. Prezentare generală

Acest articol rapid va fi o introducere în utilizarea blocului sincronizat în Java.

Pur și simplu, într-un mediu cu mai multe fire, o condiție de cursă apare atunci când două sau mai multe fire încearcă să actualizeze date partajate mutabile în același timp. Java oferă un mecanism pentru a evita condițiile cursei prin sincronizarea accesului firului la datele partajate.

O bucată de logică marcată cu sincronizat devine un bloc sincronizat, permițând executarea unui singur fir la un moment dat .

2. De ce sincronizarea?

Să luăm în considerare o condiție tipică de cursă în care calculăm suma și mai multe fire executăm metoda calculate () :

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

Și să scriem un test simplu:

@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()); }

Pur și simplu folosim un ExecutorService cu un pool de 3 fire pentru a executa calculate () de 1000 de ori.

Dacă am executa acest lucru în serie, ieșirea așteptată ar fi 1000, dar execuția noastră cu mai multe fire eșuează aproape de fiecare dată cu o ieșire reală inconsistentă, de exemplu:

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

Desigur, acest rezultat nu este neașteptat.

O modalitate simplă de a evita condiția cursei este de a face operațiunea sigură prin utilizarea cuvântului cheie sincronizat .

3. Cuvântul cheie sincronizat

Cuvântul cheie sincronizat poate fi utilizat la diferite niveluri:

  • Metode de instanță
  • Metode statice
  • Blocuri de cod

Când folosim un bloc sincronizat , Java intern folosește un monitor cunoscut și sub numele de blocare monitor sau blocare intrinsecă, pentru a oferi sincronizare. Aceste monitoare sunt legate de un obiect, astfel toate blocurile sincronizate ale aceluiași obiect pot avea un singur fir care le execută în același timp.

3.1. Metode de instanță sincronizate

Pur și simplu adăugați cuvântul cheie sincronizat în declarația metodei pentru a sincroniza metoda:

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

Observați că, odată ce sincronizăm metoda, testul trece, cu ieșirea reală la 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()); }

Metodele de instanță sunt sincronizate peste instanța clasei care deține metoda. Ceea ce înseamnă că un singur fir pentru fiecare instanță a clasei poate executa această metodă.

3.2. Metode Stati c sincronizate

Metodele statice sunt sincronizate la fel ca metodele de instanță:

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

Aceste metode sunt sincronizate pe obiectul de clasă asociat clasei și întrucât există un singur obiect de clasă per JVM per clasă, numai un fir poate fi executat în interiorul unei metode statice sincronizate pe clasă, indiferent de numărul de instanțe pe care le are.

Să-l testăm:

@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. Blocuri sincronizate în cadrul metodelor

Uneori nu dorim să sincronizăm întreaga metodă, ci doar câteva instrucțiuni din cadrul acesteia. Acest lucru poate fi realizat prin aplicarea sincronizată la un bloc:

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

Să testăm schimbarea:

@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()); }

Observați că am trecut un parametru acest lucru la sincronizat blocul. Acesta este obiectul monitorului, codul din interiorul blocului se sincronizează cu obiectul monitorului. Pur și simplu, doar un fir pentru fiecare obiect de monitor poate fi executat în interiorul acelui bloc de cod.

În cazul în care metoda este statică , am trece numele clasei în locul referinței obiectului. Și clasa ar fi un monitor pentru sincronizarea blocului:

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

Să testăm blocul din metoda statică :

@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. Reîncadrare

Blocarea din spatele metodelor și blocurilor sincronizate este reintroducătoare. Adică, firul curent poate dobândi același blocaj sincronizat din nou în timp ce îl ține:

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"); } } }

După cum se arată mai sus, în timp ce ne aflăm într-un bloc sincronizat , putem achiziționa în mod repetat aceeași blocare a monitorului.

4. Concluzie

În acest articol rapid, am văzut diferite moduri de utilizare a cuvântului cheie sincronizat pentru a realiza sincronizarea firului.

De asemenea, am explorat modul în care o condiție de cursă poate avea impact asupra aplicației noastre și modul în care sincronizarea ne ajută să evităm acest lucru. Pentru mai multe informații despre siguranța firelor folosind încuietori în Java, consultați articolul nostru java.util.concurrent.Locks .

Codul complet pentru acest tutorial este disponibil pe GitHub.