Нить і синхронізація

проблеми багатопоточності

Проблема перша - доступ до одного ресурсу з декількох потоків. Ми вже описували проблему з однієї лопатою. Можете розширити варіант - є один бак з водою (з одним краником), 25 спраглих пити рудокопів і 5 кухлів на всіх. Доведеться домовлятися, інакше вбивство може початися. Причому треба не тільки зберегти гуртки в цілісності - треба ще організувати все так, щоб всім вдалося попити. Це частково переходить на проблему номер два.
Проблема друга - синхронізація взаємодії. Якось мені запропонували завдання - написати просту програму, щоб два потоки грали в пінг-понг. Один пише «Пінг», а другий - «Понг». Але вони це повинні робити по черзі. А тепер уявімо, що треба зробити таку ж задачу, але на 4 потоки - граємо пара на пару.

Тобто постановка проблем вельми нескладна. Раз - треба організувати упорядкований і безпечний доступ до ресурсу. Два - треба виконувати потоки в якийсь черговості.
Справа за реалізацією. І ось тут нас підстерігає багато складнощів, про які з предиханіем і говорять (і може не дарма). Почнемо з ресурсу, що.

Спільний ресурс для декількох потоків

Пропоную відразу продемонструвати проблему на нескладному прикладі. Його завдання - запустити 200 потоків класу CounterThread. Кожен потік отримує посилання на один єдиний об'єкт Counter. В процесі виконання потік викликає у цього об'єкта метод increaseCounter одну тисячу разів. Метод збільшує змінну counter на 1. Запустивши 200 потоків ми очікуємо їх закінчення (просто засипаємо на 1 секунду - цього цілком достатньо). І в кінці друкуємо результат. Подивіться код - по-моєму, там все досить прозоро:

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

Чарівне слово - synchronized

Що можна зробити для того, щоб позбутися від ситуації, в яку ми потрапили з нашими чудовими потоками. Давайте для початку трохи поміркуємо. Коли ми приходимо в магазин, то для оплати ми підходимо до каси. Касир одночасно обслуговує лише одну людину. Ми всі шикуємося до неї в чергу. По суті каса стає ексклюзивним ресурсом, яким може скористатися одночасно тільки один покупець. У многопоточности пропонується точно такий же спосіб - ви можете визначити певний ресурс як екслюзивно надається одночасно тільки одному потоку. Такий ресурс називається «монітором». Це самий звичайний об'єкт, який потік повинен «захопити». Всі потоки, які хочуть отримати доступ до цього монітора (об'єкту) шикуються в чергу. Причому для цього не треба писати спеціальний код - досить просто спробувати «захопити» монітор. Але як же позначити це. Давайте розбиратися.
Пропоную запустити наш приклад, але з одним додатковим словом в описі методу increaseCounter - це слово synchronized.

Окреме використання synchronized робить по суті те ж саме - спочатку блокує / лочіт переданий йому в дужках об'єкт і потім починає виконувати код, який знаходиться всередині synchronized. Тут треба врахувати, що блокування буде відбувається не на вході в метод, а при вході в блок synchronized. До речі за рахунок цього можна зробити синхронізацію всередині одного об'єкта за кількома. Наприклад, два методу блокують один об'єкт, а два інші - інший. Тоді ви зможете викликати методи з різних груп одночасно. Я таке використовував у своїй практиці.
В принципі ідея synchronized на цьому вичерпується. Тепер важливим стає правильне його використання. Тому що з одного боку може виникнути бажання все методи робити synchronized. але це буде впливати на продуктивність - думаю, що це очевидно. З іншого боку - можуть виникнути складності при необережному поводженні з несинхронізованих об'єктами. Так що будьте пильні.

Потокобезпечна класи - thread safe

Метод getCounter можна просто оголосити в документації як потоконебезопасний. А якщо в класі Counter реалізувати метод setCounter, то робити ці два методи синхронізованими не має ніякого сенсу.

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

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

Таке буває іноді - я не розбирався в причинах.

Виклик Thread.sleep () зупиняє поточний потік. Тобто ті 200, що запущені, чесно відпрацювали. А потік всередині методу main чекає.

Шановний Admin, чи правильно я розумію, що є потік всередині методу main, в якому (в потоці) запускається код (цикл), в свою чергу запускає 200 потоків, які відпрацьовують; при цьому ми ставимо на паузу потік методу main, щоб він дочекався відпрацювання цих 200 потоків перед тим як сам (метод main) завершить свою дію.
Або ж потік методу main запускається, паралельно з ним запускаються наші 200; Thread.sleep (1000) дає можливість їм відпрацювати, потім потік методу main поновлюється, викидає в консоль повідомлення і далі закривається.
Вибачте за питання, в початковому курсі Java ви все це «розжовувати»?

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

Таке завдання: є МФУ, воно може друк або сканувати одночасно. Є два принтера і два сканера. Написати програму так, щоб в один момент часу МФУ був доступний тільки одного принтера і одному сканеру, але не двом принтерів \ сканерів одночасно. Тобто в один момент часу може друкуватися сторінка, і паралельно скануватися, але не можна одночасно друкувати \ сканувати дві сторінки. Робота принтера \ сканера виводиться в консоль: «принт ..», «скан ..». Якось так, кривовато написано, хто зрозумів - поясніть детальніше.

Перший приклад працює без synchronized якщо основний потік main чекатиме завершення породжених їм потоків до рядка виведення значення змінної counter за допомогою виклику join () на породжених потоках в циклі main. Як не дивно працює не тільки з типом int, а й long змінної counter.

Leave a reply Cancel reply

Схожі статті