Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Лекція Введення в потоки Java

.docx
Скачиваний:
8
Добавлен:
08.10.2015
Размер:
41.64 Кб
Скачать

Контроль доступу за допомогою синхронізації

Мова Java пропонує два ключових слова для контролю загального доступу до даних: synchronized і volatile.

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

При відсутності синхронізації дані легко можуть залишитися в неузгоджену стані. Наприклад, якщо один потік оновлює два взаємопов'язаних значення (скажімо, координату і швидкість частинки), а інший потік читає ці два значення, може виявитися так, що перший потік, будучи запущеним після другого, запише одне значення, але не встигне записати другий, в внаслідок чого другий потік прочитає одне нове значення і одне старе. Синхронізація дозволяє визначити блоки коду, які повинні виконуватися атомарне, в тому сенсі, що з точки зору інших потоків вони повинні виконуватися за принципом «все або нічого».

Атомарному виконання або взаємне виключення подібно концепції критичних секцій в інших операційних середовищах.

Забезпечення видимості змін загальних даних

Синхронізація дозволяє гарантувати, що потоки бачать узгоджене уявлення пам'яті.

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

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

Ключове слово Volatile простіше, ніж синхронізація, і годиться лише для управління доступом до одного екземпляра примітивних змінних - цілих, логічних і т.п. Якщо змінна оголошена як volatile, будь-який запис в цю змінну буде виконуватися безпосередньо в основну пам'ять, минаючи кеш, і читання цієї змінної буде виконуватися безпосередньо з основної пам'яті, також минаючи кеш. Це означає, що в будь-який момент часу всі потоки будуть бачити одне й те ж значення змінної volatile.

Без відповідної синхронізації можливі ситуації, коли потоки будуть бачити застарілі значення змінних або страждати від будь-яких інших форм спотворення даних.

Блоки атомарного коду, захищені замками

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

Для координації доступу до конкретних блокам коду синхронізація використовує концепцію моніторів або замків.

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

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

Простий приклад синхронізації

Використання синхронізованих блоків дозволяє виконувати групу пов'язаних оновлень у вигляді однієї операції, не піклуючись про те, що інші потоки можуть перервати оновлення або побачити проміжні результати обчислень. Код, наведений у наступному прикладі, буде виводити на друк "1 0" або "0 1". Однак за відсутності синхронізації він може надрукувати "1 + 1" (або навіть "0 0", хоча в це й важко повірити).

public class SyncExample {

  private static Object lockObject = new Object ();

  private static class Thread1 extends Thread {

    public void run () {

      synchronized (lockObject) {

        x = y = 0;

        System.out.println (x);

      }

    }

  }

  private static class Thread2 extends Thread {

    public void run () {

      synchronized (lockObject) {

        x = y = 1;

        System.out.println (y);

      }

    }

  }

  public static void main (String [] args) {

    new Thread1 (). start ();

    new Thread2 (). start ();

  }

}

          

Для коректної роботи цієї програми потрібно використовувати синхронізацію в обох її потоках.

Механізм блокування в мові Java

Механізм блокування в мові Java використовує деяку форму взаємного виключення. У кожен момент часу замок може утримуватися тільки одним потоком. Замки використовуються для захисту блоків коду або цілих методів, але при цьому важливо пам'ятати, що блок коду захищається екземпляром замку, а не самим замком. Один замок може захищати кілька блоків коду або методів.

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

У наступному прикладі два потоки можуть виконувати синхронізований блок в setLastAccess () одночасно, оскільки кожен потік використовує різне значення thingie. Отже, цей синхронізований блок захищений різними замками в двох виконуються потоках.

public class SyncExample {

  public static class Thingie {

    private Date lastAccess;

    public synchronized void setLastAccess (Date date) {

      this.lastAccess = date;

    }

  }

  public static class MyThread extends Thread {

    private Thingie thingie;

    public MyThread (Thingie thingie) {

      this.thingie = thingie;

    }

    public void run () {

      thingie.setLastAccess (new Date ());

    }

  }

  public static void main () {

    Thingie thingie1 = new Thingie (),

      thingie2 = new Thingie ();

    new MyThread (thingie1) .start ();

    new MyThread (thingie2) .start ();

  }

}

          

Синхронізовані методи

Найпростішим способом створення синхронізованого блоку є оголошення методу як synchronized. Це означає, що перед входом в тіло цього методу викликає код повинен опанувати замком:

public class Point {

  public synchronized void setXY (int x, int y) {

    this.x = x;

    this.y = y;

  }

}

          

Для звичайних синхронізованих методів у ролі замку виступатиме об'єкт, до якого застосовано цей метод. Для статичних синхронізованих методів у ролі замку виступатиме монітор, пов'язаний з об'єктом Class, в якому оголошено цей метод.

Той факт, що метод setXY () оголошений як synchronized, ще не означає, що два різних потоку не можуть виконувати setXY () одночасно, якщо вони викликають setXY () для різних екземплярів Point. Але лише один потік в кожен момент часу може виконувати setXY () або будь-який інший синхронізований метод Point на одному примірнику Point.

Синхронізовані блоки

Синтаксис синхронізованих блоків дещо складніше синтаксису синхронізованих методів, оскільки вам потрібно явно вказати, яким замком захищається цей блок. Наступна версія Point еквівалентна версії, показаної вище:

public class Point {

  public void setXY (int x, int y) {

    synchronized (this) {

      this.x = x;

      this.y = y;

    }

  }

}

          

Загальноприйнятим, хоча і не обов'язковим, є використання в якості замка покажчика this. Це означає, що блок буде використовувати той же замок, що і синхронізовані методи цього класу.

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

Доступ до локальних змінних (на основі стека) зовсім не потребує захисту, оскільки вони видні тільки потоку-власнику.

Більшість класів не синхронізовано

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

Класи Collections можна використовувати в багатопотокових застосуваннях, застосовуючи синхронізацію при кожному зверненні до методу в загальній колекції. Для кожної даної колекції потрібно завжди використовувати для синхронізації один і той же замок. Дуже часто в якості замка використовується сам об'єкт колекції

Наведений нижче приклад класу SimpleCache показує, як можна використовувати HashMap для організації поточно-орієнтованого кешування. Однак, в загальному випадку, відповідна синхронізація зовсім не означає синхронізацію кожного методу.

Клас Collections пропонує набір зручних пакувальників для інтерфейсів List, Map і Set. Ви можете упакувати Map в Collections.synchronizedMap, що гарантуватиме відповідну синхронізацію всіх звернень до цього Map.

Якщо в документації на клас не згадується, що він поточно-орієнтований, ви зобов'язані виходити з припущення, що він таким не є.

Приклад: простий поточно-орієнтований кеш

Як показано в наступному прикладі коду, SimpleCache.java використовує HashMap для створення простого кеша для об'єкта loader. Метод load () вміє завантажувати об'єкт за його ключу. Після одноразової завантаження об'єкта він зберігається в кеші, тому подальші звернення будуть витягати його з кеша замість того, щоб кожен раз повторювати завантаження. Кожне звернення до загального кешу захищається синхронізованим блоком. Завдяки відповідної синхронізації декілька потоків можуть викликати методи getObject і clearCache одночасно без ризику пошкодження даних.

public class SimpleCache {

  private final Map cache = new HashMap ();

  public Object load (String objectName) {

    // Завантаження деякого об'єкту

  }

  public void clearCache () {

    synchronized (cache) {

      cache.clear ();

    }

  }

  public Object getObject (String objectName) {

    synchronized (cache) {

      Object o = cache.get (objectName);

      if (o == null) {

        o = load (objectName);

        cache.put (objectName, o);

      }

    }

    return o;

  }

}

          

 

Підіб'ємо підсумки

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

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

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

Детальніше про синхронізацію

Взаємне виключення

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

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

Видимість

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

Аналогічним чином, перед звільненням замку об'єкт повинен скинути кеш, щоб усі зміни відбилися в основній пам'яті.

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

Коли застосовувати синхронізацію?

Для забезпечення відповідної видимості у всіх потоках слід використовувати ключове слово synchronized (або volatile), що гарантує, що зміни, внесені одним потоком, будуть видні іншому потоку, якщо деяка що не відноситься до типу final змінна спільно використовується декількома потоками.

Основним правилом використання синхронізації для забезпечення видимості є її застосування в тих випадках, коли ви:

Cчітиваете змінну, яку міг змінити інший потік

Записуєте змінну, яка може читатися іншим потоком

Застосування синхронізації для забезпечення узгодженості

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

Давайте розглянемо наступний приклад, який реалізує простий (але не поточно-орієнтований) стек цілих чисел:

public class UnsafeStack {

  public int top = 0;

  public int [] values ​​= new int [1000];

  public void push (int n) {

    values ​​[top ++] = n;

  }

  public int pop () {

    return values ​​[- top];

  }

}

          

Якщо декілька потоків спробують одночасно використовувати цей клас, може статися катастрофа. Через відсутність синхронізації декілька потоків можуть одночасно виконувати операції push () і pop (). Що станеться, якщо один потік викличе push (), а інший потік викличе push () в проміжок часу між инкрементування змінної top і застосуванням її в якості індексу для values? У цьому випадку обидва потоки збережуть нове значення в одному і тому ж місці! Це всього лише один з багатьох можливих варіантів пошкодження даних, яке може статися, якщо кілька потоків покладаються на відому взаємозв'язок між значеннями даних, але немає гарантії того, що в кожен момент часу цими значеннями маніпулює лише один потік.

В даному випадку допоможе просте ліки: синхронізуйте push () і pop (), і ви уникнете конфлікти потоків.

Зверніть увагу, що застосування ключового слова volatile при цьому недостатньо - потрібно використовувати ключове слово synchronized, щоб гарантувати злагоджену взаємозв'язок між top і values.

Инкрементування загального лічильника

У загальному випадку, якщо ви захищаєте одну примітивну змінну, таку як ціле число, іноді достатньо ключового слова volatile. Однак якщо нові значення змінної виходять з попередніх значень, ви повинні використовувати синхронізацію. Чому? Погляньте на такий приклад:

public class Counter {

  private int counter = 0;

  public int get () {return counter; }

  public void set (int n) {counter = n; }

  public void increment () {

    set (get () + 1);

  }

}

          

Що станеться, якщо ми захочемо інкрементувати лічильник? Подивіться на код increment (). Він простий, але не поточно-орієнтований. Що станеться, якщо два потоки спробують виконати increment () одночасно? Лічильник може змінити значення на 1 або на 2. Як не дивно, оголошення counter як volatile не допоможе вирішити цю проблему, не допоможе і оголошення get () і set () як synchronized.

Уявіть, що лічильник встановлений в нуль і два потоки одночасно виконують код инкрементування. Обидва потоки викликають Counter.get () і бачать, що значення лічильника дорівнює нулю. Тепер обидва потоку збільшують це значення на одиницю і потім викликають Counter.set (). При невдалому збігу обставин жоден з потоків не побачить поновлення іншого потоку, навіть якщо counter оголошений як volatile або get (), а set () оголошені як synchronized. Тепер, навіть якщо ми інкрементуємо лічильник двічі, його значення може виявитися рівним одиниці, а не двом.

Щоб інкрементування працювало коректно, потрібно синхронізувати не тільки get () і set (), але й increment ()! В іншому випадку потік, що викликає increment (), може перервати інший потік, також викликає increment (). При невдалому збігу обставин лічильник інкрементується один раз замість двох. Синхронізація increment () виключає таку можливість.

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

Незмінність і поля final

Багато класів Java, включаючи String, Integer і BigDecimal, є незмінними: після того, як вони сконструйовані, їх стан ніколи не змінюється. Клас буде незмінним, якщо всі його поля оголошені як final. (На практиці багато незмінні класи мають поля, не оголошені як final, для кешування результатів роботи попереднього методу, подібного String.hashCode (), проте викликає процес цього не бачить.)

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

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

Коли не потрібно застосовувати синхронізацію

Існує ряд випадків, коли для передачі даних з одного потоку в інший синхронізація не потрібна, оскільки вона в неявній формі виконується самою JVM. До їх числа відносяться наступні:

Ініціалізація даних статичним ініціалізаторів (ініціалізаторів статичного поля або ініціалізаторів в блоці static {})

Доступ до полів final

Створення об'єкта до створення потоку

Ситуація, коли об'єкт вже видно потоку, з яким він потім об'єднується

Взаємоблокування

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

Найпоширеніша форма взаємоблокування полягає в тому, що потік 1 утримує замок на об'єкті А і чекає звільнення замка на об'єкті Б, а потік 2 утримує замок на об'єкті Б і чекає звільнення замка на об'єкті А. Жоден з потоків ніколи не отримає доступу до другого замку і ніколи не відпустить перший. Вони будуть чекати нескінченно.

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

Проблеми продуктивності

Про вплив синхронізації на продуктивність написано багато, і велика частина написаного - неправда. Варто визнати, що синхронізація, особливо конкурентна, впливає на продуктивність, але не настільки, як багато хто припускає.

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

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

Рекомендації щодо синхронізації

Нижче наведено кілька простих рекомендацій, яким варто слідувати при написанні синхронізованих блоків і які допоможуть уникнути ризику виникнення взаємоблокувань і зниження продуктивності:

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

Не допускайте блокування. Ніколи не викликайте метод, який може блокуватися, такий як InputStream.read (), усередині синхронізованого блоку або методу.

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

Додаткові відомості про API потоку

Методи wait (), notify () і notifyAll ()

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

Клас Object визначає методи wait (), notify () і notifyAll (). Щоб виконати будь-який з цих методів, ви повинні утримувати замок відповідного об'єкта.

Wait () викликає засипання викликає потоку, поки він не буде розбуджений методом Thread.interrupt (), поки не закінчиться зазначений таймаут або поки його не розбудить інший потік методом notify () або notifyAll ().

Якщо при застосуванні до об'єкта методу notify () існують потоки, що чекають цей об'єкт через wait (), один потік буде розбуджений. Якщо до об'єкта застосований метод notifyAll (), то будуть розбуджені всі потоки, які очікують цей об'єкт.

Ці методи є будівельними блоками більш складного коду, що використовує блокування, черги і паралельне виконання. Однак застосування методів notify () і notifyAll () пов'язане з певними труднощами. Зокрема, застосування notify () замість notifyAll () пов'язане з деяким ризиком. Використовуйте notify () лише тоді, коли точно знаєте, що робите.

Замість використання wait () і notify () для створення своїх власних планувальників, пулів потоків, черг і замків, краще скористатися пакетом util.concurrent (див. Розділ Ресурси), широко поширеним інструментарієм з відкритим вихідним кодом, повним корисних паралельних утиліт. JDK 1.5 буде містити пакет java.util.concurrent, багато класи якого взяті з util.concurrent.

Пріоритети потоків

Інтерфейс Thread API дозволяє прив'язати до кожного потоку деякий пріоритет виконання. Однак спосіб розподілу пріоритетів в планувальнику операційної системи залежить від конкретної реалізації.

Багато хто намагається маніпулювати пріоритетами потоків, стикаючись з такими проблемами, як взаємоблокування, дефіцит або з іншими проблемами планувальника. Однак у більшості випадків це лише переносить проблему з одного місця в інше. У більшості програм просто слід уникати зміни пріоритету потоків.

Групи потоків

Спочатку вважалося, що клас ThreadGroup буде корисний для об'єднання колекцій потоків в групи. Однак виявилося, що потенційна користь ThreadGroup перебільшена. Правильніше буде використовувати еквівалентні методи в Thread.

І все-таки ThreadGroup має одну корисну функцію, не представлену (поки що) в Thread: метод uncaughtException (). Якщо потік усередині групи потоків завершує роботу через те, що створив необроблюваних виняток, викликається метод ThreadGroup.uncaughtException (). Це дає можливість завершити роботу системи, записати повідомлення в журнал або перезапустити службу, що викликала помилку.

Клас SwingUtilities

Хоча клас SwingUtilities не є частиною API Thread, він заслуговує короткої згадки.

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

Метод SwingUtilities.invokeLater () дозволяє передати йому об'єкт Runnable, при цьому зазначений Runnable буде виконаний в потоці обробки подій. Споріднений йому метод invokeAndWait () також буде викликати Runnable в потоці обробки подій, але invokeAndWait () буде блокуватися, поки Runnable не закінчить виконання.

void showHelloThereDialog () throws Exception {

    Runnable showModalDialog = new Runnable () {

        public void run () {

            JOptionPane.showMessageDialog (myMainFrame, "Hello There");

        }

    };

    SwingUtilities.invokeLater (showModalDialog);

}

          

Крім того, для додатків AWT java.awt.EventQueue пропонує методи invokeLater () і invokeAndWait ().

Wrapup

Висновок

Кожна програма Java використовує потоки, знаєте ви про це чи ні. Якщо ви використовуєте Java-інструментарії для користувача інтерфейсу (AWT або Swing), сервлети Java, RMI, сторінки JavaServer або технологію Enterprise JavaBeans, то ви, мабуть, вже використовуєте потоки, хоча й не підозрюєте про це.

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

Підвищення чуйності інтерфейсу користувача під час виконання тривалих завдань

Застосування багатопроцесорних систем для паралельного управління декількома завданнями

Спрощення моделювання або систем на основі агентів

Виконання асинхронної або фонової обробки

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

При використанні синхронізації для захисту загальних змінних вам потрібно не тільки застосовувати синхронізацію, але і стежити за тим, щоб читач і пишучий процеси синхронізувалися від одного і того ж монітора. Крім того, якщо ви покладаєтеся на те, що стан об'єкта залишиться незмінним протягом кількох операцій або розраховуєте, що кілька змінних залишаться узгодженими між собою (або зі своїми попередніми значеннями), ви повинні використовувати синхронізацію, щоб це гарантовано сталося. Але проста синхронізація всіх методів класу не робить його поточно-орієнтованим - вона лише робить його більш схильним до виникнення взаємоблокування.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]