Вступ до синхронізації на Java

Синхронізація - це функція Java, яка обмежує декілька потоків одночасно намагатися отримати доступ до загальнодоступних ресурсів. Тут спільні ресурси посилаються на вміст зовнішніх файлів, змінні класу або записи бази даних.

Синхронізація широко використовується в багатопотоковому програмуванні. "Синхронізований" - це ключове слово, яке надає вашому коду можливість дозволити йому працювати лише один потік без втручання будь-якого іншого потоку протягом цього періоду.

Навіщо нам потрібна синхронізація на Java?

  • Java - це багатопотокова мова програмування. Це означає, що дві або більше ниток можуть працювати одночасно до виконання завдання. Коли потоки працюють одночасно, є велика ймовірність виникнення сценарію, коли ваш код може забезпечити несподівані результати.
  • Вам може бути цікаво, що якщо багатопотокове читання може спричинити помилкові виходи, то чому це вважається важливою особливістю Java?
  • Багатопотокова швидкість робить ваш код швидшим, паралельно запускаючи кілька потоків і тим самим скорочуючи час виконання коду та забезпечуючи високу продуктивність. Однак використання багатопотокового середовища призводить до неточних результатів через стан, загальновідоме як умова гонки.

Що таке умова гонки?

Коли два або більше потоків працюють паралельно, вони схильні отримувати доступ і змінювати спільні ресурси в цей момент часу. Послідовності, в яких виконуються потоки, визначаються алгоритмом планування потоків.

Через це не можна передбачити порядок виконання потоків, оскільки ним керує виключно планувальник потоків. Це впливає на вихід коду і призводить до невідповідних результатів. Оскільки декілька потоків перегоняються між собою для завершення операції, умова називається "гоночною умовою".

Для прикладу розглянемо наступний код:

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

При послідовному виконанні вищевказаного коду результати будуть наступними:

Наш вихід1:

Поточна нитка, що виконується потоком 1 Поточне значення нитки 3

Поточна нитка, що виконується потоком 3 Поточне значення нитки 2

Поточна нитка, що виконується потоком 2 Поточне значення нитки 3

Вихід2:

Поточна нитка, що виконується потоком 3 Поточне значення нитки 3

Поточна нитка, що виконується потоком 2 Поточне значення нитки 3

Поточна нитка, що виконується потоком 1 Поточне значення нитки 3

Вихід3:

Поточна нитка, що виконується потоком 2 Поточне значення нитки 3

Поточна нитка, що виконується потоком 1 Поточне значення нитки 3

Поточна нитка, що виконується потоком 3 Поточне значення нитки 3

Вихід4:

Поточна нитка, що виконується потоком 1 Поточне значення нитки 2

Поточна нитка, що виконується потоком 3 Поточне значення нитки 3

Поточна нитка, що виконується потоком 2 Поточне значення нитки 2

  • З наведеного вище прикладу можна зробити висновок, що потоки виконуються навмання, а також значення є неправильним. Згідно з нашою логікою, значення слід збільшувати на 1. Однак тут вихідне значення в більшості випадків становить 3, а в кількох випадках - 2.
  • Тут змінна "myVar" - це спільний ресурс, на якому виконується кілька потоків. Нитки отримують доступ та змінюють значення "myVar" одночасно. Давайте подивимося, що станеться, якщо ми прокоментуємо дві інші теми.

Вихід у цьому випадку:

Поточна нитка, що виконується потоком 1 Поточне значення 1

Це означає, що коли працює один потік, результат є таким, як очікувалося. Однак, коли працює кілька потоків, значення змінюється кожним потоком. Тому потрібно обмежувати кількість потоків, що працюють над спільним ресурсом, лише одним потоком. Це досягається за допомогою синхронізації.

Розуміння, що таке синхронізація в Java

  • Синхронізація в Java досягається за допомогою ключового слова "синхронізований". Це ключове слово можна використовувати для методів, блоків або об'єктів, але не може бути використане для класів та змінних. Синхронізований фрагмент коду дозволяє лише одному потоку отримати доступ та змінити його в даний момент часу.
  • Однак синхронізований фрагмент коду впливає на продуктивність коду, оскільки збільшує час очікування інших потоків, які намагаються отримати доступ до нього. Таким чином, фрагмент коду слід синхронізувати лише тоді, коли є шанс виникнення перегонової умови. Якщо ні, не слід уникати цього.

Як синхронізація в Java працює внутрішньо?

  • Внутрішня синхронізація в Java реалізована за допомогою концепції блокування (також відомої як монітор). Кожен об’єкт Java має власний замок. У синхронізованому блоці коду потік повинен придбати замок, перш ніж мати змогу виконати конкретний блок коду. Як тільки нитка придбає замок, вона може виконати цей фрагмент коду.
  • По завершенні виконання він автоматично звільняє замок. Якщо інший потік потребує роботи над синхронізованим кодом, він чекає, поки поточний потік, що працює на ньому, звільнить замок. Цей процес придбання та звільнення замків внутрішньо опікується віртуальною машиною Java. Програма не несе відповідальності за придбання та звільнення замків потоком. Однак інші потоки можуть одночасно виконувати будь-який інший несинхронізований фрагмент коду.

Давайте синхронізуємо наш попередній приклад, синхронізувавши код всередині методу запуску за допомогою синхронізованого блоку класу «Змінити», як показано нижче:

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

Код для класу “RaceCondition” залишається незмінним. Тепер, запускаючи код, вихід виглядає наступним чином:

Вихід1:

Поточна нитка, що виконується потоком 1 Поточне значення 1

Поточна нитка, що виконується потоком 2, значення поточної нитки 2

Поточна нитка, що виконується потоком 3 Поточне значення нитки 3

Вихід2:

Поточна нитка, що виконується потоком 1 Поточне значення 1

Поточна нитка, що виконується ниткою 3 Поточне значення нитки 2

Поточна нитка, що виконується потоком 2, значення поточної нитки 3

Зверніть увагу, що наш код забезпечує очікуваний вихід. Тут кожен потік збільшує значення на 1 для змінної “myVar” (у класі “Змінити”).

Примітка. Синхронізація потрібна, коли на одному об'єкті працює кілька потоків. Якщо на декількох об'єктах працює кілька потоків, синхронізація не потрібна.

Наприклад, давайте змінимо код у класі “RaceCondition”, як показано нижче, і працюємо з раніше несинхронізованим класом “Змінити”.

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

Вихід:

Поточна нитка, що виконується потоком 1 Поточне значення 1

Поточна нитка, що виконується ниткою 2 Поточне значення 1

Поточна нитка, що виконується потоком 3 Поточне значення 1

Типи синхронізації на Java:

Існує два типи синхронізації потоків: один взаємовиключний, а другий міжпотоковий зв'язок.

1.Взаємовиключні

  • Синхронізований метод.
  • Статичний синхронізований метод
  • Синхронізований блок.

2. Координація нитки (міжпотокове спілкування в java)

Взаємовиключними:

  • У цьому випадку потоки отримують блокування перед тим, як працювати над об'єктом, тим самим уникаючи роботи з об'єктами, які мали свої значення маніпулювати іншими потоками.
  • Цього можна досягти трьома способами:

i. Синхронізований метод: Ми можемо використовувати ключове слово «синхронізований» для методу, таким чином, роблячи його синхронізованим методом. Кожен потік, який викликає синхронізований метод, отримає замок для цього об'єкта і відпустить його після завершення його роботи. У наведеному вище прикладі ми можемо зробити наш метод "run ()" синхронізованим за допомогою ключового слова "синхронізований" після модифікатора доступу.

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

Вихід для цього випадку буде:

Поточна нитка, що виконується потоком 1 Поточне значення 1

Поточна нитка, що виконується ниткою 3 Поточне значення нитки 2

Поточна нитка, що виконується потоком 2, значення поточної нитки 3

ii. Статичний синхронізований метод: Для синхронізації статичних методів потрібно придбати блокування рівня класу. Після отримання потоку блоку рівня класу лише тоді він зможе виконати статичний метод. Хоча потік містить блокування рівня класу, жоден інший потік не може виконати будь-який інший метод статичного синхронізації цього класу. Однак інші потоки можуть виконувати будь-який інший регулярний метод або звичайний статичний метод або навіть нестатичний синхронізований метод цього класу.

Наприклад, давайте розглянемо наш клас «Змінити» та внесемо зміни до нього, перетворивши метод «збільшення» в статичний синхронізований метод. Зміни коду наведені нижче:

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. Синхронізований блок: Одним з головних недоліків синхронізованого методу є те, що він збільшує час очікування потоків, що впливає на продуктивність коду. Тому, щоб мати можливість синхронізувати лише необхідні рядки коду замість всього методу, потрібно використовувати синхронізований блок. Використання синхронізованого блоку скорочує час очікування ниток і також покращує продуктивність. У попередньому прикладі ми вже використовували синхронізований блок під час синхронізації нашого коду вперше.

Приклад:
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());
)
)

Координація нитки:

Для синхронізованих потоків важливим завданням є міжпотокове спілкування. Вбудовані методи, що допомагають досягти міжпотокового зв'язку для синхронізованого коду, а саме:

  • почекати ()
  • сповістити ()
  • notifyAll ()

Примітка. Ці методи належать до класу об'єктів, а не до класу потоків. Щоб потік міг викликати ці методи на об'єкті, він повинен тримати замок на цьому об'єкті. Також ці методи змушують нитку звільняти свій замок на об'єкт, на який він викликається.

wait (): Нитка при виклику методу wait (), звільняє замок на об'єкті і переходить у стан очікування. Він має два способи перевантаження:

  • публічне остаточне недійсне очікування () кидає InterruptedException
  • публічне остаточне недійсне очікування (тривалий тайм-аут) кидає InterruptedException
  • публічне остаточне недійсне очікування (long timeout, int nanos) кидає InterruptedException

notify (): потік передає сигнал іншому потоку в стані очікування, використовуючи метод notify (). Він надсилає сповіщення лише одному потоку, щоб цей потік міг відновити його виконання. Який потік отримає сповіщення серед усіх потоків у стані очікування, залежить від віртуальної машини Java.

  • публічне остаточне повідомлення про недійсність ()

notifyAll (): Коли потік викликає метод notifyAll (), кожен потік у стані очікування повідомляється. Ці потоки будуть виконуватися одна за одною відповідно до порядку, визначеного віртуальною машиною Java.

  • публічне остаточне недійсне повідомленняAll ()

Висновок

У цій статті ми бачили, як робота в багатопотоковому середовищі може призвести до невідповідності даних через стан перегонів. Як синхронізація допомагає нам подолати це, обмеживши один потік для роботи на спільному ресурсі за раз. Також те, як синхронізовані потоки спілкуються між собою.

Рекомендовані статті:

Це було керівництвом щодо того, що таке синхронізація на Java ?. Тут ми обговорюємо вступ, розуміння, потребу, роботу та типи синхронізації з деяким зразковим кодом. Ви також можете ознайомитися з іншими запропонованими нами статтями, щоб дізнатися більше -

  1. Серіалізація на Java
  2. Що таке дженерики на Java?
  3. Що таке API в Java?
  4. Що таке бінарне дерево на Яві?
  5. Приклади та як дженерики працюють у C #