JavaRush /Java блог /Random UA /Синхронізація потоків. Оператор synchronized у Java

Синхронізація потоків. Оператор synchronized у Java

Стаття з групи Random UA
Вітання! Сьогодні продовжимо розглядати особливості багатопотокового програмування та поговоримо про синхронізацію потоків.
Синхронізація потоків.  Оператор synchronized - 1
Що таке «синхронізація»? Поза областю програмування під цим мається на увазі налаштування, що дозволяє двом пристроям чи програмам працювати спільно. Наприклад, смартфон та комп'ютер можна синхронізувати з Google-акаунтом, особистий кабінет на сайті — з обліковцями в соціальних мережах, щоб логінуватися за їх допомогою. У синхронізації потоків схоже значення: це налагодження взаємодії потоків між собою. У попередніх лекціях наші потоки жабо та працювали окремо один від одного. Один щось вважав, другий спав, третій виводив щось на консоль, але вони не взаємодіяли. У реальних програмах такі ситуації рідкісні. Декілька потоків можуть активно працювати, наприклад, з тим самим набором даних і щось у ньому змінювати. Це створює проблеми. Уяви, що кілька потоків записують текст в те саме місце — наприклад, в текстовий файл або консоль. Цей файл чи консоль у разі стає загальним ресурсом. Потоки не знають про існування один одного, тому просто записують усе, що встигнуть за час, який планувальник потоків їм виділить. У недавній лекції курсу ми мали приклад, до чого це приведе, давай його згадаємо: Синхронізація потоків.  Оператор synchronized - 2Причина у тому, що потоки працювали із загальним ресурсом, консоллю, не узгоджуючи дії друг з одним. Якщо планувальник потоків виділив час Поток-1, той моментально пише все в консоль. Що там уже встигли чи не встигли написати інші потоки — не має значення. Результат, як бачиш, плачевний. Тому в багатопотоковому програмуванні ввели спеціальне поняття м'ютекс (від англ. Mutex, mutual exclusion - взаємний виняток) . Завдання мьютексу— забезпечити такий механізм, щоб доступ до об'єкта у певний час мав лише один поток. Якщо Потік-1 захопив м'ютекс об'єкта А, решта потоків не отримає до нього доступу, щоб щось у ньому змінювати. Доки м'ютекс об'єкта А не звільниться, інші потоки будуть змушені чекати. Приклад із життя: уяви, що ти та ще 10 незнайомих людей берете участь у тренінгу. Вам потрібно по черзі висловлювати ідеї та щось обговорювати. Але, оскільки один одного ви бачите вперше, щоб постійно не перебивати один одного і не скочуватися в гвалт, ви використовуєте правило з «м'ячиком, що говорить»: говорити може тільки одна людина — та, у кого в руках м'ячик. Так дискусія виходить адекватною та плідною. Так от м'ютекс, по суті, і є такий м'ячик. Якщо м'ютекс об'єкта знаходиться в руках одного потоку, інші потоки не зможуть отримати доступ до цього об'єкта. Не потрібно нічого робити, щоб створити м'ютекс: він уже вбудований у класObject, а значить, є у кожного об'єкта Java.

Як працює оператор synchronized у Java

Давай познайомимося з новим ключовим словом – synchronized . Їм позначається певний шматок нашого коду. Якщо блок коду позначений ключовим словом synchronized, це означає, що блок може виконуватися лише одним потоком одночасно. Синхронізацію можна реалізувати по-різному. Наприклад, створити цілий синхронізований метод:
public synchronized void doSomething() {

   //...логіка методу
}
Або ж написати блок коду, де синхронізація здійснюється за якимось об'єктом:
public class Main {

   private Object obj = new Object();

   public void doSomething() {

       //...якась логіка, доступна всім потоків

       synchronized (obj) {

           // Логіка, яка одночасно доступна тільки для одного потоку
       }
   }
}
Сенс простий. Якщо один потік зайшов усередину блоку коду, який позначений словом synchronized, він моментально захоплює м'ютекс об'єкта, і всі інші потоки, які спробують зайти в цей же блок або метод, змушені чекати, поки попередній потік не завершить свою роботу і не звільнить монітор. Синхронізація потоків.  Оператор synchronized - 3До речі! У лекціях курсу ти вже бачив приклади synchronized, але вони виглядали інакше:
public void swap()
{
   synchronized (this)
   {
       //...логіка методу
   }
}
Тема для тебе нова, і плутанина із синтаксисом, само собою, спочатку буде. Тому запам'ятай одразу, щоб не плутатися потім у способах написання. Ці два способи запису означають те саме:
public void swap() {

   synchronized (this)
   {
       //...логіка методу
   }
}


public synchronized void swap() {

   }
}
У першому випадку створюєш синхронізований блок коду відразу ж при вході до методу. Він синхронізується за об'єктом this, тобто за поточним об'єктом. А в другому прикладі вішаєш слово synchronized на весь метод. Тут немає потреби явно вказувати якийсь об'єкт, яким здійснюється синхронізація. Якщо словом позначений цілий метод, цей метод автоматично буде синхронізованим для всіх об'єктів класу. Не заглиблюватимемося в міркування, який спосіб кращий. Поки вибирай те, що більше подобається :) Головне пам'ятай: оголосити метод синхронізованим можна тільки тоді, коли вся логіка всередині нього виконується одним потоком одночасно. Наприклад, у цьому випадку зробити метод doSomething()синхронізованим буде помилкою:
public class Main {

   private Object obj = new Object();

   public void doSomething() {

       //...якась логіка, доступна всім потоків

       synchronized (obj) {

           // Логіка, яка одночасно доступна тільки для одного потоку
       }
   }
}
Як бачиш, шматочок методу містить логіку, для якої синхронізація не є обов'язковою. Код у ньому можуть виконувати кілька потоків одночасно, а всі критично важливі місця виділені в окремий блок synchronized. І ще один момент. Давай розглянемо «під мікроскопом» наш приклад із лекції з обміном іменами:
public void swap()
{
   synchronized (this)
   {
       //...логіка методу
   }
}
Зверніть увагу: синхронізація проводиться по this. Тобто щодо конкретного об'єкту MyClass. Уяви, що у нас є 2 потоки ( Thread-1і Thread-2) і всього один об'єкт MyClass myClass. У разі, якщо Thread-1викличе метод myClass.swap(), м'ютекс об'єкта буде зайнятий, і Thread-2за спробі викликати myClass.swap()повисне чекаючи, коли мьютекс звільниться. Якщо ж ми матимемо 2 потоки і 2 об'єкти MyClassmyClass1і myClass2— на різних об'єктах наші потоки спокійно зможуть одночасно виконувати синхронізовані методи. Перший потік виконує:
myClass1.swap();
Другий виконує:
myClass2.swap();
У цьому випадку ключове слово synchronized усередині методу swap()не вплине на роботу програми, оскільки синхронізація здійснюється за конкретним об'єктом. А в останньому випадку об'єктів у нас 2. Тому потоки не створюють один одного проблем. Адже два об'єкти мають 2 різні м'ютекси, і їх захоплення не залежить один від одного.

Особливості синхронізації у статичних методах

А що робити, якщо необхідно синхронізувати статичний метод?
class MyClass {
   private static String name1 = "Оля";
   private static String name2 = "Ліна";

   public static synchronized void swap() {
       String s = name1;
       name1 = name2;
       name2 = s;
   }

}
Незрозуміло, що виконуватиме роль м'ютексу в цьому випадку. Адже ми вже визначабося, що кожен об'єкт має м'ютекс. Але проблема в тому, що для виклику статичного методу MyClass.swap()нам не потрібні об'єкти: метод статичний! І що далі? :/ Насправді проблеми в цьому немає. Творці Java про все подбали :) Якщо метод, у якому міститься критично важлива «багатопотокова» логіка, статичний, синхронізація буде здійснюватися за класом. Для більшої ясності наведений вище код можна переписати так:
class MyClass {
   private static String name1 = "Оля";
   private static String name2 = "Ліна";

   public static void swap() {

       synchronized (MyClass.class) {
           String s = name1;
           name1 = name2;
           name2 = s;
       }
   }

}
У принципі, ти міг до цього додуматися самостійно: якщо об'єктів немає, значить механізм синхронізації має бути якось «зашитий» у самі класи. Так воно і є: за класами також можна синхронізуватися.
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ