Przewodnik po zsynchronizowanych słowach kluczowych w Javie

1. Przegląd

Ten krótki artykuł będzie wprowadzeniem do korzystania z zsynchronizowanego bloku w Javie.

Mówiąc najprościej, w środowisku wielowątkowym sytuacja wyścigu występuje, gdy dwa lub więcej wątków próbuje zaktualizować modyfikowalne dane udostępnione w tym samym czasie. Java oferuje mechanizm pozwalający uniknąć sytuacji wyścigu poprzez synchronizację dostępu wątków do udostępnionych danych.

Fragment logiki oznaczony jako zsynchronizowany staje się zsynchronizowanym blokiem, pozwalającym na wykonanie tylko jednego wątku w danym momencie .

2. Dlaczego synchronizacja?

Rozważmy typowy stan wyścigu, w którym obliczamy sumę, a wiele wątków wykonuje metodę oblicz () :

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

I napiszmy prosty test:

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

Używamy po prostu usługi ExecutorService z pulą 3 wątków, aby 1000 razy wykonać funkcję calc () .

Gdybyśmy wykonali to szeregowo, oczekiwany wynik wyniósłby 1000, ale nasze wielowątkowe wykonanie kończy się niepowodzeniem prawie za każdym razem z niespójnymi rzeczywistymi danymi wyjściowymi, np .:

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

Ten wynik nie jest oczywiście nieoczekiwany.

Prostym sposobem uniknięcia sytuacji wyścigu jest zapewnienie bezpieczeństwa wątku operacji przy użyciu słowa kluczowego synchronized .

3. Zsynchronizowane słowo kluczowe

Zsynchronizowane kluczowe mogą być stosowane na różnych poziomach:

  • Metody instancji
  • Metody statyczne
  • Bloki kodu

Kiedy używamy zsynchronizowanego bloku, Java wewnętrznie używa monitora znanego również jako blokada monitora lub blokada wewnętrzna, aby zapewnić synchronizację. Te monitory są powiązane z obiektem, więc wszystkie zsynchronizowane bloki tego samego obiektu mogą mieć tylko jeden wątek wykonujący je w tym samym czasie.

3.1. Zsynchronizowane metody instancji

Po prostu dodaj słowo kluczowe synchronized w deklaracji metody, aby zsynchronizować metodę:

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

Zauważ, że po zsynchronizowaniu metody przypadek testowy przechodzi, a rzeczywiste dane wyjściowe to 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()); }

Metody instancji są synchronizowane z instancją klasy będącej właścicielem metody. Oznacza to, że tylko jeden wątek na wystąpienie klasy może wykonać tę metodę.

3.2. Zsynchronizowane Stati C Metody

Metody statyczne są synchronizowane tak samo jak metody instancji:

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

Metody te są synchronizowane w obiekcie Class powiązanym z klasą, a ponieważ istnieje tylko jeden obiekt Class na maszynę JVM na klasę, tylko jeden wątek może być wykonywany wewnątrz statycznej metody synchronicznej na klasę, niezależnie od liczby jej instancji.

Przetestujmy to:

@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. Zsynchronizowane bloki w metodach

Czasami nie chcemy synchronizować całej metody, a jedynie niektóre instrukcje w niej zawarte. Można to osiągnąć, stosując zsynchronizowane z blokiem:

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

Przetestujmy zmianę:

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

Zauważ, że przekazaliśmy parametr this do zsynchronizowanego bloku. To jest obiekt monitora, kod wewnątrz bloku zostaje zsynchronizowany z obiektem monitora. Mówiąc najprościej, tylko jeden wątek na obiekt monitora może być wykonywany wewnątrz tego bloku kodu.

W przypadku, gdy metoda jest statyczna , przekazalibyśmy nazwę klasy w miejsce odwołania do obiektu. A klasa byłaby monitorem synchronizacji bloku:

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

Przetestujmy blok wewnątrz metody statycznej :

@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. Ponowne wejście

Blokada za zsynchronizowanymi metodami i blokami jest ponownie wprowadzana. Oznacza to, że bieżący wątek może uzyskać tę samą zsynchronizowaną blokadę w kółko, trzymając go:

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

Jak pokazano powyżej, gdy jesteśmy w zsynchronizowanym bloku, możemy wielokrotnie uzyskać tę samą blokadę monitora.

4. Wniosek

W tym krótkim artykule widzieliśmy różne sposoby używania słowa kluczowego synchronized do synchronizacji wątków.

Zbadaliśmy również, jak sytuacja wyścigu może wpłynąć na naszą aplikację i jak synchronizacja pomaga nam tego uniknąć. Więcej informacji na temat bezpieczeństwa wątków przy użyciu blokad w Javie można znaleźć w naszym artykule java.util.concurrent.Locks .

Pełny kod tego samouczka jest dostępny w serwisie GitHub.