Wprowadzenie do synchronizacji w Javie

Synchronizacja to funkcja Java, która ogranicza wiele wątków przed próbą uzyskania dostępu do często współużytkowanych zasobów w tym samym czasie. Tutaj współdzielone zasoby odnoszą się do zewnętrznej zawartości pliku, zmiennych klas lub rekordów bazy danych.

Synchronizacja jest szeroko stosowana w programowaniu wielowątkowym. „Synchronizowane” jest słowem kluczowym, które zapewnia Twojemu kodowi możliwość zezwalania na działanie tylko jednego wątku bez interwencji jakiegokolwiek innego wątku w tym okresie.

Dlaczego potrzebujemy synchronizacji w Javie?

  • Java to wielowątkowy język programowania. Oznacza to, że dwa lub więcej wątków może działać jednocześnie w celu zakończenia zadania. Gdy wątki działają jednocześnie, istnieje duże prawdopodobieństwo wystąpienia scenariusza, w którym kod może zapewnić nieoczekiwane wyniki.
  • Możesz się zastanawiać, że jeśli wielowątkowość może powodować błędne dane wyjściowe, dlaczego jest to ważna funkcja w Javie?
  • Wielowątkowość przyspiesza kod, uruchamiając wiele wątków równolegle, a tym samym skracając czas wykonywania kodów i zapewniając wysoką wydajność. Jednak korzystanie ze środowiska wielowątkowości prowadzi do niedokładnych danych wyjściowych z powodu stanu powszechnie znanego jako stan wyścigu.

Co to jest stan wyścigu?

Gdy dwa lub więcej wątków działa równolegle, mają one tendencję do uzyskiwania dostępu i modyfikowania zasobów współdzielonych w tym momencie. Kolejność wykonywania wątków jest ustalana przez algorytm planowania wątków.

Z tego powodu nie można przewidzieć kolejności wykonywania wątków, ponieważ jest ona kontrolowana wyłącznie przez program do planowania wątków. Wpływa to na wyjście kodu i powoduje niespójne wyniki. Ponieważ wiele wątków ściga się ze sobą, aby ukończyć operację, warunek ten określa się jako „warunek wyścigowy”.

Na przykład rozważmy poniższy kod:

Class Modify:
package JavaConcepts;
public class Modify implements Runnable(
private int myVar=0;
public int getMyVar() (
return myVar;
)
public void setMyVar(int myVar) (
this.myVar = myVar;
)
public void increment() (
myVar++;
)
@Override
public void run() (
// TODO Auto-generated method stub
this.increment();
System.out.println("Current thread being executed "+ Thread.currentThread().getName() + "Current Thread value " + this.getMyVar());
)
)
Class RaceCondition:
package JavaConcepts;
public class RaceCondition (
public static void main(String() args) (
Modify mObj = new Modify();
Thread t1 = new Thread(mObj, "thread 1");
Thread t2 = new Thread(mObj, "thread 2");
Thread t3 = new Thread(mObj, "thread 3");
t1.start();
t2.start();
t3.start();
)
)

Po kolejnym uruchomieniu powyższego kodu wyniki będą następujące:

Ourput1:

Aktualnie wykonywany wątek wątek 1 Aktualna wartość wątku 3

Aktualnie wykonywany wątek wątek 3 Aktualna wartość wątku 2

Aktualnie wykonywany wątek wątek 2 Aktualna wartość wątku 3

Wyjście 2:

Aktualnie wykonywany wątek wątek 3 Aktualna wartość wątku 3

Aktualnie wykonywany wątek wątek 2 Aktualna wartość wątku 3

Aktualnie wykonywany wątek wątek 1 Aktualna wartość wątku 3

Wyjście3:

Aktualnie wykonywany wątek wątek 2 Aktualna wartość wątku 3

Aktualnie wykonywany wątek wątek 1 Aktualna wartość wątku 3

Aktualnie wykonywany wątek wątek 3 Aktualna wartość wątku 3

Wyjście4:

Aktualnie wykonywany wątek wątek 1 Aktualna wartość wątku 2

Aktualnie wykonywany wątek wątek 3 Aktualna wartość wątku 3

Aktualnie wykonywany wątek wątek 2 Aktualna wartość wątku 2

  • Z powyższego przykładu można wywnioskować, że wątki są wykonywane losowo, a także wartość jest niepoprawna. Zgodnie z naszą logiką wartość powinna być zwiększana o 1. Jednak tutaj wartość wyjściowa w większości przypadków wynosi 3, aw kilku przypadkach 2.
  • Tutaj zmienna „myVar” jest zasobem współdzielonym, na którym wykonuje się wiele wątków. Wątki jednocześnie uzyskują dostęp do wartości „myVar” i modyfikują ją. Zobaczmy, co się stanie, jeśli skomentujemy pozostałe dwa wątki.

Dane wyjściowe w tym przypadku to:

Aktualnie wykonywany wątek 1 wątek Bieżąca wartość wątku 1

Oznacza to, że gdy działa pojedynczy wątek, dane wyjściowe są zgodne z oczekiwaniami. Jednak gdy działa wiele wątków, wartość jest modyfikowana przez każdy wątek. Dlatego należy ograniczyć liczbę wątków pracujących na współużytkowanym zasobie do jednego wątku na raz. Osiąga się to za pomocą synchronizacji.

Zrozumienie, czym jest synchronizacja w Javie

  • Synchronizację w Javie uzyskuje się za pomocą słowa kluczowego „synchronized”. Tego słowa kluczowego można używać do metod, bloków lub obiektów, ale nie można go stosować z klasami i zmiennymi. Zsynchronizowany fragment kodu pozwala na dostęp i modyfikację tylko jednego wątku w danym momencie.
  • Jednak zsynchronizowany fragment kodu wpływa na wydajność kodu, ponieważ wydłuża czas oczekiwania innych wątków próbujących uzyskać do niego dostęp. Dlatego fragment kodu powinien być zsynchronizowany tylko wtedy, gdy istnieje szansa na wystąpienie warunków wyścigu. Jeśli nie, należy tego unikać.

Jak działa synchronizacja w Javie wewnętrznie?

  • Wewnętrzna synchronizacja w Javie została zaimplementowana za pomocą koncepcji blokady (znanej również jako monitor). Każdy obiekt Java ma własną blokadę. W zsynchronizowanym bloku kodu wątek musi uzyskać blokadę, zanim będzie mógł wykonać ten konkretny blok kodu. Gdy wątek uzyska blokadę, może wykonać ten fragment kodu.
  • Po zakończeniu wykonania automatycznie zwalnia blokadę. Jeśli inny wątek wymaga działania na zsynchronizowanym kodzie, czeka, aż bieżący działający na nim wątek zwolni blokadę. Proces pozyskiwania i zwalniania blokad jest wewnętrznie obsługiwany przez maszynę wirtualną Java. Program nie ponosi odpowiedzialności za uzyskiwanie i zwalnianie blokad przez wątek. Pozostałe wątki mogą jednak wykonywać jednocześnie dowolny inny niezsynchronizowany fragment kodu.

Zsynchronizujmy nasz poprzedni przykład, synchronizując kod w metodzie uruchamiania za pomocą zsynchronizowanego bloku w klasie „Modyfikuj”, jak poniżej:

Class Modify:
package JavaConcepts;
public class Modify implements Runnable(
private int myVar=0;
public int getMyVar() (
return myVar;
)
public void setMyVar(int myVar) (
this.myVar = myVar;
)
public void increment() (
myVar++;
)
@Override
public void run() (
// TODO Auto-generated method stub
synchronized(this) (
this.increment();
System.out.println("Current thread being executed "
+ Thread.currentThread().getName() + " Current Thread value " + this.getMyVar());
)
)
)

Kod dla klasy „RaceCondition” pozostaje taki sam. Teraz po uruchomieniu kodu dane wyjściowe są następujące:

Wyjście 1:

Aktualnie wykonywany wątek 1 wątek Bieżąca wartość wątku 1

Aktualnie wykonywany wątek wątek 2 Aktualna wartość wątku 2

Aktualnie wykonywany wątek wątek 3 Aktualna wartość wątku 3

Wyjście 2:

Aktualnie wykonywany wątek 1 wątek Bieżąca wartość wątku 1

Aktualnie wykonywany wątek wątek 3 Aktualna wartość wątku 2

Aktualnie wykonywany wątek wątek 2 Aktualna wartość wątku 3

Zauważ, że nasz kod zapewnia oczekiwany wynik. Tutaj każdy wątek zwiększa wartość o 1 dla zmiennej „myVar” (w klasie „Modify”).

Uwaga: Synchronizacja jest wymagana, gdy wiele wątków działa na tym samym obiekcie. Jeśli wiele wątków działa na wielu obiektach, synchronizacja nie jest wymagana.

Na przykład zmodyfikujmy kod w klasie „RaceCondition”, jak poniżej, i pracuj z poprzednio niezsynchronizowaną klasą „Modify”.

package JavaConcepts;
public class RaceCondition (
public static void main(String() args) (
Modify mObj = new Modify();
Modify mObj1 = new Modify();
Modify mObj2 = new Modify();
Thread t1 = new Thread(mObj, "thread 1");
Thread t2 = new Thread(mObj1, "thread 2");
Thread t3 = new Thread(mObj2, "thread 3");
t1.start();
t2.start();
t3.start();
)
)

Wynik:

Aktualnie wykonywany wątek 1 wątek Bieżąca wartość wątku 1

Aktualnie wykonywany wątek wątek 2 Aktualna wartość wątku 1

Aktualnie wykonywany wątek wątek 3 Aktualna wartość wątku 1

Rodzaje synchronizacji w Javie:

Istnieją dwa rodzaje synchronizacji wątków, jedna wyklucza się wzajemnie, a druga komunikacja między wątkami.

1. wyłącznie ekskluzywne

  • Metoda zsynchronizowana.
  • Metoda zsynchronizowana statycznie
  • Zsynchronizowany blok.

2. Koordynacja wątków (komunikacja między wątkami w java)

Wzajemnie się wykluczające:

  • W takim przypadku wątki uzyskują blokadę przed operacją na obiekcie, co pozwala uniknąć pracy z obiektami, których wartości zostały zmanipulowane przez inne wątki.
  • Można to osiągnąć na trzy sposoby:

ja. Metoda zsynchronizowana: Możemy użyć słowa kluczowego „synchronized” dla metody, dzięki czemu jest to metoda zsynchronizowana. Każdy wątek, który wywołuje metodę synchroniczną, uzyska blokadę dla tego obiektu i zwolni go po zakończeniu operacji. W powyższym przykładzie możemy wykonać synchronizację naszej metody „run ()” za pomocą słowa kluczowego „synchronized” po modyfikatorze dostępu.

@Override
public synchronized void run() (
// TODO Auto-generated method stub
this.increment();
System.out.println("Current thread being executed "
+ Thread.currentThread().getName() + " Current Thread value " + this.getMyVar());
)

Dane wyjściowe dla tego przypadku będą:

Aktualnie wykonywany wątek 1 wątek Bieżąca wartość wątku 1

Aktualnie wykonywany wątek wątek 3 Aktualna wartość wątku 2

Aktualnie wykonywany wątek wątek 2 Aktualna wartość wątku 3

ii. Metoda zsynchronizowana statycznie: Aby zsynchronizować metody statyczne, należy uzyskać blokadę poziomu klasy. Gdy wątek uzyska blokadę na poziomie klasy, będzie mógł wykonać metodę statyczną. Podczas gdy wątek posiada blokadę poziomu klasy, żaden inny wątek nie może wykonać żadnej innej statycznej synchronicznej metody tej klasy. Jednak inne wątki mogą wykonywać dowolną inną zwykłą metodę lub zwykłą metodę statyczną, a nawet niestatyczną metodę synchroniczną tej klasy.

Rozważmy na przykład naszą klasę „Modyfikuj” i wprowadzamy w niej zmiany, konwertując naszą metodę „inkrementacji” na statyczną metodę synchroniczną. Zmiany w kodzie są następujące:

package JavaConcepts;
public class Modify implements Runnable(
private static int myVar=0;
public int getMyVar() (
return myVar;
)
public void setMyVar(int myVar) (
this.myVar = myVar;
)
public static synchronized void increment() (
myVar++;
System.out.println("Current thread being executed " + Thread.currentThread().getName() + " Current Thread value " + myVar);
)
@Override
public void run() (
// TODO Auto-generated method stub
increment();
)
)

iii. Blok zsynchronizowany: Jedną z głównych wad metody zsynchronizowanej jest to, że wydłuża czas oczekiwania wątków, wpływając na wydajność kodu. Dlatego, aby móc zsynchronizować tylko wymagane wiersze kodu zamiast całej metody, należy skorzystać ze zsynchronizowanego bloku. Zastosowanie synchronizowanego bloku skraca czas oczekiwania wątków i poprawia również wydajność. W poprzednim przykładzie korzystaliśmy już z synchronizowanego bloku podczas synchronizacji naszego kodu po raz pierwszy.

Przykład:
public void run() (
// TODO Auto-generated method stub
synchronized(this) (
this.increment();
System.out.println("Current thread being executed "
+ Thread.currentThread().getName() + " Current Thread value " + this.getMyVar());
)
)

Koordynacja wątków:

W przypadku wątków synchronicznych ważna jest komunikacja między wątkami. Wbudowane metody, które pomagają osiągnąć komunikację między wątkami dla zsynchronizowanego kodu to:

  • czekać()
  • notyfikować()
  • replaceAll ()

Uwaga: Te metody należą do klasy obiektowej, a nie do klasy wątkowej. Aby wątek mógł wywoływać te metody na obiekcie, powinien trzymać blokadę na tym obiekcie. Ponadto metody te powodują zwolnienie wątku na obiekcie, na którym jest wywoływane.

wait (): Wątek wywołania metody wait (), zwalnia blokadę obiektu i przechodzi w stan oczekiwania. Ma dwa przeciążenia metod:

  • publiczne końcowe void wait () zgłasza InterruptedException
  • publiczne końcowe anulowanie nieważności (długi limit czasu) generuje InterruptedException
  • publiczne końcowe anulowanie nieważności (długi limit czasu, int nanos) zgłasza wyjątek InterruptedException

powiadomienie (): Wątek wysyła sygnał do innego wątku w stanie oczekiwania za pomocą metody notyfikacji (). Wysyła powiadomienie tylko do jednego wątku, aby ten wątek mógł wznowić wykonywanie. Który wątek otrzyma powiadomienie między wszystkimi wątkami w stanie oczekiwania, zależy od wirtualnej maszyny Java.

  • publiczne nieważne powiadomienie ()

replaceAll (): Gdy wątek wywołuje metodę replaceAll (), każdy wątek w stanie oczekiwania jest powiadamiany. Wątki te będą wykonywane jeden po drugim na podstawie kolejności ustalonej przez maszynę wirtualną Java.

  • publiczne ostateczne nieważne powiadomienieAll ()

Wniosek

W tym artykule widzieliśmy, jak praca w środowisku wielowątkowym może prowadzić do niespójności danych z powodu warunków wyścigu. W jaki sposób synchronizacja pomaga nam to przezwyciężyć, ograniczając pojedynczy wątek do pracy na wspólnym zasobie jednocześnie. Także w jaki sposób synchronizowane wątki komunikują się ze sobą.

Polecane artykuły:

To był przewodnik po Co to jest synchronizacja w Javie ?. Tutaj omawiamy wprowadzenie, zrozumienie, potrzebę, działanie i rodzaje synchronizacji z jakimś przykładowym kodem. Możesz także przejrzeć nasze inne sugerowane artykuły, aby dowiedzieć się więcej -

  1. Serializacja w Javie
  2. Co to jest Generics w Javie?
  3. Co to jest API w Javie?
  4. Co to jest drzewo binarne w Javie?
  5. Przykłady i jak działają generics w C #