Синхронізація проти блокування


182

java.util.concurrentAPI надає клас, який називається як Lock, який би в основному серіалізував керування для доступу до критичного ресурсу. Він дає такий метод, як park()і unpark().

Ми можемо зробити подібні речі, якщо зможемо використовувати synchronizedключове слово, використовуючи wait()і notify() notifyAll()методи.

Мені цікаво, що з них краще на практиці і чому?


1
корисна стаття тут: javarevisited.blogspot.in/2013/03/…
roottraveller

Відповіді:


178

Якщо ви просто блокуєте об'єкт, я вважаю за краще використовувати synchronized

Приклад:

Lock.acquire();
doSomethingNifty(); // Throws a NPE!
Lock.release(); // Oh noes, we never release the lock!

Ви повинні прямо робити try{} finally{}скрізь.

У той час як із синхронізованими помилково зрозуміти і неможливо:

synchronized(myObject) {
    doSomethingNifty();
}

Однак, Locks може бути кориснішим для складніших речей, коли ви не можете придбати та випустити настільки чистий спосіб. Я б чесно вважав за краще уникати використання голих Lockспершу, а просто перейти з більш досконалим контролем одночасності, таким як CyclicBarrierабоLinkedBlockingQueue , якщо вони відповідають вашим потребам.

У мене ніколи не було причин використовувати wait()або, notify()можливо, є якісь хороші.


1
Яка різниця між очікуванням / сповіщенням проти парку / відпаркуванням LockSupport? docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/…
Pacerier

6
Спочатку приклад мав сенс із блокуваннями, але потім я зрозумів, що якщо використовувати спробу остаточно заблокувати цю проблему, уникнути не вдасться, щоб блокування не було випущено
William Reed,

А-а-а ... Один із тих моментів, коли можна оцінити модель RAII в C ++. std::lock_guard
WhiZTiM

67

Мені цікаво, що з них краще на практиці і чому?

Я знайшов, що Lockі Condition(та інші новіconcurrent класи) - це просто більше інструментів для інструментальної групи. Я міг зробити все, що мені потрібно, зі своїм старим молоточком ( synchronizedключове слово), але це було незручно використовувати в деяких ситуаціях. Деякі з таких незручних ситуацій стали набагато простішими, коли я додав у свій інструментарій більше інструментів: гумовий молоток, молоток з кулькою, стрижень та деякі удари для нігтів. Однак мій старий молоточок з кігтями все ще бачить свою частку використання.

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

===

Я думаю, що JavaDoc робить гарну роботу, щоб описати відмінність між Lockта synchronized(акцент - мій):

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

...

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

Хоча механізм визначення розміру синхронізованих методів та операторів значно спрощує програмування з блокуваннями монітора та допомагає уникнути багатьох поширених помилок програмування, пов’язаних із блокуваннями, є випадки, коли вам потрібно працювати з блокуваннями більш гнучко. Наприклад, * * деякі алгоритми * для проходження одночасно доступних структур даних вимагають використання "передачі передачі" або "блокування ланцюга" : ви отримуєте блокування вузла A, потім вузол B, потім випускаєте A і отримуєте C, потім відпустіть B і придбайте D тощо. Реалізація інтерфейсу Lock дозволяє використовувати подібні методи, дозволяючи придбати та випустити декілька блокувань у будь-якому порядку . дозволяючи придбати та випустити замок у різних сферах , та

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

...

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

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

...


23

Ви можете досягти всі комунальні послуги в java.util.concurrent робити з низькорівневих примітивів , таких як synchronized, volatileабо очікування / сповіщати

Однак паралельність складна, і більшість людей помиляються хоча б на деяких частинах, роблячи код або неправильним, або неефективним (або обом).

Паралельний API забезпечує підхід більш високого рівня, який простіше (і як такий безпечніше) у використанні. Якщо коротко, вам більше не потрібно використовувати synchronized, volatile, wait, notifyбезпосередньо.

Сам клас Lock знаходиться на нижньому рівні цієї панелі інструментів, можливо, вам навіть не доведеться використовувати це безпосередньо (ви можете використовувати Queuesі Semaphore, і речі тощо, більшість часу).


2
Чи звичайне старе очікування / сповіщення вважається примітивом нижчого рівня, ніж java.util.concurrent.locks.LockSupport park / unpark, чи це навпаки?
Pacerier

@Pacerier: Я вважаю обидва низькорівневі (тобто те, що програміст додатків хотів би уникати прямого використання), але, безумовно, частини нижнього рівня java.util.concurrency (наприклад, пакет замків) будуються зверху уроджених примітивів JVM чекають / сповіщають (які ще нижчого рівня).
Тіло

2
Ні , я маю в виду з 3: Thread.sleep / переривання, Object.wait / повідомить LockSupport.park / распарки, який є найбільш примітивним?
Пейсьер

2
@Thilo Я не впевнений, як ти підтримуєш своє твердження, яке java.util.concurrentпростіше [загалом], ніж мовні функції ( synchronizedтощо) ... Коли ви користуєтеся, java.util.concurrentви повинні звикнути заповнювати lock.lock(); try { ... } finally { lock.unlock() }перед тим, як писати код, тоді як з a - у synchronizedвас все добре з самого початку. На цій основі я б сказав, що synchronizedце простіше (якщо ви хочете, щоб його поведінка), ніж java.util.concurrent.locks.Lock. пар 4
Iwan Aucamp

1
Не думайте, що ви можете точно копіювати поведінку класів AtomicXXX лише за допомогою однотипних примітивів, оскільки вони покладаються на вихідний виклик CAS, недоступний до java.util.concurrent.
Дункан Армстронг

15

Є 4 основні фактори, чому ви хочете використовувати synchronizedабо java.util.concurrent.Lock.

Примітка. Синхронізоване блокування - це те, що я маю на увазі, коли кажу про внутрішнє блокування.

  1. Коли Java 5 вийшла з ReentrantLocks, у них виявилася досить помітна різниця пропускної здатності, ніж внутрішня блокування. Якщо ви шукаєте швидший механізм блокування та працює у 1.5, розгляньте jucReentrantLock. Власне блокування Java 6 тепер порівнянне.

  2. jucLock має різні механізми блокування. Блокування переривним - намагайтеся зафіксувати, поки фіксуюча нитка не буде перервана; приурочений замок - намагайтеся заблокувати певний час і відмовитися, якщо вам це не вдалося; tryLock - спроба заблокувати, якщо якась інша нитка тримає замок, відмовтеся. Це все включено окрім простого замка. Внутрішнє блокування пропонує лише просте блокування

  3. Стиль. Якщо і 1, і 2 не належать до категорій того, що вас хвилює щодо більшості людей, у тому числі і я, виявило б, що внутрішня семантика блокування є легшою для читання та менш докладною, а потім jucLock.
  4. Кілька умов. Об'єкт, на який ви заблокуєте, може бути повідомлений і чекати лише одного випадку. Новий метод стану Lock дозволяє одному блоку мати неоднозначні причини чекати або подавати сигнал. Мені ще справді потрібна ця функціональність на практиці, але це приємна особливість для тих, хто її потребує.

Мені сподобалися деталі у вашому коментарі. Я додам ще одну точку кулі - ReadWriteLock забезпечує корисну поведінку, якщо ви маєте справу з декількома потоками, лише деякі з яких потрібно записати в об’єкт. Кілька потоків можуть одночасно читати об’єкт і блокуються лише у тому випадку, якщо інший потік вже пише на нього.
Сем Голдберг

5

Я хотів би додати ще кілька речей на вершині Bert F відповіді.

Locksпідтримують різні методи для тонкого зернистого управління замком, які є більш виразними, ніж неявні монітори ( synchronizedблокування)

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

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

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

  2. Реалізації блокування надають додаткову функціональність щодо використання синхронізованих методів та висловлювань, забезпечуючи неблокуючу спробу придбати a lock (tryLock()), спробу придбати замок, який можна перервати ( lockInterruptibly()та спробу придбати замок, який може timeout (tryLock(long, TimeUnit)).

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

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

ReentrantLockОсновні характеристики відповідно до цієї статті

  1. Можливість переривання блокування.
  2. Можливість тайм-ауту під час очікування блокування.
  3. Потужність для створення справедливого замка.
  4. API для отримання списку очікування для блокування.
  5. Гнучкість приміряти замок без блокування.

Можна використовувати ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock для подальшого отримання контролю над зернистим блокуванням операцій читання та запису.

Крім цих трьох ReentrantLocks, java 8 забезпечує ще один Блокування

StampedLock:

Java 8 постачається з новим видом блокування під назвою StampedLock, який також підтримує блокування читання та запису, як у прикладі вище. На відміну від ReadWriteLock, способи блокування StampedLock повертають штамп, представлений довгим значенням.

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

Подивіться цю статтю про використання різних типів ReentrantLockта StampedLockзамків.


4

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

synchronized(foo) {
}

або

lock.acquire(); .....lock.release();

не гарантує справедливості.

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

Щоб отримати повне обговорення цієї теми, див. Книгу Брайана Гетца "Конкурс Java на практиці", розділ 13.3.


5
"Синхронізація рівня методів забезпечує справедливе або FIFO розподіл блокування." => Дійсно? Ви хочете сказати, що синхронізований метод поводиться інакше wrt справедливості, ніж загортання вмісту методів у синхронізований блок {}? Я б не думав, чи я зрозумів це речення неправильно ...?
weiresr

Так, хоча це дивно і протилежне інтуїтивному правильному. Книга Геца - найкраще пояснення.
Брайан Тарбокс

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

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

Вибачте, я щойно виявив, що я помилково проголосив цю відповідь кілька днів тому (незграбне клацання). На жаль, на даний момент SO не дозволить мені відновити його. Сподіваюся, я можу це виправити пізніше.
Mikko Östlund

3

Книга Брайана Гетца "Конкурс Java на практиці", розділ 13.3: "... Як і за замовчуванням ReentrantLock, внутрішнє блокування не дає жодних детермінованих гарантій справедливості, але гарантії справедливості статистики більшості реалізацій блокування досить хороші для майже всіх ситуацій ..."


2

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

  1. Блокуйте в одному методі, а вимкніть замок в іншому.
  2. Якщо у вас є два потоки, що працюють над двома різними фрагментами коду, однак у першому потоці є попередній реквізит для певного фрагмента коду у другому потоці (тоді як деякі інші потоки також працюють над тим самим фрагментом коду у другому нитка одночасно). Спільний замок може вирішити цю проблему досить легко.
  3. Впровадження моніторів. Наприклад, проста черга, де методи put і get виконуються з багатьох інших потоків. Однак ви не хочете одночасно працювати декількома методами put (або get), а також методом put і get одночасно. Приватний замок значно полегшує ваше життя.

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


1

Основна різниця між блокуванням та синхронізованим:

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

0

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

void randomFunction(){
.
.
.
synchronize(this){
//do some functionality
}

.
.
.
synchronize(this)
{
// do some functionality
}


} // end of randomFunction

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.