Чому треба чекати () завжди бути в синхронізованому блоці


257

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

Який потенційний збиток, якщо можна було викликати wait()поза синхронізованого блоку, зберігаючи його семантику - призупинення потоку виклику?

Відповіді:


232

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

Семантично ти ніколи не просто wait(). Вам потрібна якась умова, щоб бути задоволеною, і якщо її немає, ви чекаєте, поки вона стане. Тож те, що ти насправді робиш

if(!condition){
    wait();
}

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

Ще кілька помилок з цим, коли тільки те, що ваша нитка перестала чекати, не означає, що ви шукаєте умову:

  • Ви можете отримати помилкові пробудження (це означає, що нитка може прокинутися від очікування, не отримавши повідомлення), або

  • Умова може бути встановлена, але третя нитка робить умову знову помилковою до часу пробудження потоку очікування (і повторно придбає монітор).

Щоб розібратися з цими справами, те, що вам справді потрібно, - це завжди деякі зміни цього:

synchronized(lock){
    while(!condition){
        lock.wait();
    }
}

А ще краще - не возитися із примітивами синхронізації і працювати з абстракціями, які пропонуються в java.util.concurrentпакунках.


3
Тут також є детальна дискусія, яка говорить по суті те саме. coding.derkeiler.com/Archive/Java/comp.lang.java.programmer/…

1
btw, якщо ви не ігноруєте перерваний прапор, цикл також перевірятиметься Thread.interrupted().
bestsss

2
Я все ще можу зробити щось на кшталт: while (! Умова) {синхронізований (це) {wait ();}}, що означає, що все ще існує гонка між перевіркою умови та очікуванням, навіть якщо wait () правильно викликається в синхронізованому блоці. То чи є якась інша причина цього обмеження, можливо, завдяки тому, як воно реалізується на Java?
shrini1000

9
Ще один неприємний сценарій: умова хибна, ми збираємося перейти в режим wait (), а потім інший потік змінює умову і викликає notify (). Оскільки ми ще не чекаємо (), ми пропустимо це сповіщення (). Іншими словами, перевірка та зачекання, а також зміна та повідомлення повинні бути атомними .

1
@Nullpointer: Якщо це тип aa, який можна записати атомно (наприклад, булевий сенс, використовуючи його безпосередньо в пункті if), і немає взаємозалежності з іншими спільними даними, ви можете піти, оголосивши його мінливим. Але вам це потрібно або синхронізація, щоб оновлення було видно в інших потоках негайно.
Майкл Боргвардт

282

Який потенційний збиток, якщо можна було викликати wait()поза синхронізованого блоку, зберігаючи його семантику - призупинення потоку виклику?

Проілюструємо, з якими проблемами ми стикалися б, якщо їх wait()можна було назвати поза синхронізованим блоком на конкретному прикладі .

Припустимо, ми мали впровадити чергу блокування (я знаю, в API вже є така :)

Перша спроба (без синхронізації) може виглядати щось у нижченаведеному рядку

class BlockingQueue {
    Queue<String> buffer = new LinkedList<String>();

    public void give(String data) {
        buffer.add(data);
        notify();                   // Since someone may be waiting in take!
    }

    public String take() throws InterruptedException {
        while (buffer.isEmpty())    // don't use "if" due to spurious wakeups.
            wait();
        return buffer.remove();
    }
}

Це те, що потенційно може статися:

  1. Споживча нитка дзвонить take()і бачить, що buffer.isEmpty().

  2. Перед тим, як споживча нитка продовжує дзвонити wait(), приходить продуктивний потік і викликає повне give(), тобтоbuffer.add(data); notify();

  3. Тепер споживча нитка зателефонує wait()пропустить те, notify()що було щойно зателефоновано).

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

Як тільки ви зрозумієте проблему, рішення очевидно: Використовуйте, synchronizedщоб переконатися, що notifyніколи не викликається між isEmptyі wait.

Не вдаючись до деталей: Ця проблема синхронізації є універсальною. Як зазначає Майкл Боргвардт, чекати / сповіщати - це все про зв'язок між потоками, тож ви завжди будете в кінцівці з умовою гонки, подібною до описаної вище. Ось чому застосовується правило "тільки чекати все синхронізоване".


Абзац із посилання, розміщеного на @Willie, досить добре підсумовує його:

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

Приклад, про який погоджуються виробник та споживач, є у наведеному вище прикладі buffer.isEmpty(). І угода вирішується, забезпечуючи виконання synchronizedблоку очікування та сповіщення .


Ця публікація була переписана як стаття тут: Java: Чому чекати потрібно викликати в синхронізованому блоці


Крім того, щоб переконатися, що зміни, внесені до умови, помітні відразу після закінчення wait (), я думаю. В іншому випадку також мертвий замок, оскільки повідомлення notify () вже викликано.
Surya Wijaya Madjid

Цікаво, але зауважте, що просто виклик синхронізованим насправді не завжди вирішить подібні проблеми через "ненадійний" характер wait () та notify (). Детальніше тут: stackoverflow.com/questions/21439355 / ... . Причина синхронізації потрібна в апаратній архітектурі (див. Мою відповідь нижче).
Маркус

але якщо додати return buffer.remove();в той час як блок, але після wait();, він працює?
BobJiang

@BobJiang, ні, нитку можна прокинути з інших причин, ніж хтось закликає давати. Іншими словами, буфер може бути порожнім навіть після waitповернення.
aioobe

Я функціоную лише Thread.currentThread().wait();в mainоточенні пробного лову InterruptedException. Без synchronizedблоку це дає мені той самий виняток IllegalMonitorStateException. Що змушує зараз досягти незаконної держави? synchronizedОднак він працює всередині блоку.
Шашват

12

@Rollerball має рацію. wait()Називаються, так що нитка може почекати деякий умова відбувається , коли це wait()відбувається виклик, потік змушений відмовитися від свого замку.
Щоб відмовитись від чогось, потрібно спочатку володіти ним. Ниткою потрібно спочатку володіти замком. Звідси необхідність викликати його всередині synchronizedметоду / блоку.

Так, я погоджуюся з усіма вищезазначеними відповідями щодо можливих пошкоджень / невідповідностей, якщо ви не перевіряли стан у synchronizedметоді / блоці. Однак, як вказував @ shrini1000, просто виклик wait()у синхронізованому блоці не запобіжить цьому невідповідності.

Ось приємне прочитання ..


5
@Popeye Поясніть "належним чином" належним чином. Ваш коментар нікому не корисний.
Маркіз Лорнський

4

Проблема, яка може виникнути, якщо ви не синхронізувались раніше, wait()полягає в наступному:

  1. Якщо перший потік входить makeChangeOnX()і перевіряє стан while, і він true( x.metCondition()повертається false, значить, x.conditionє false), то він потрапить всередину нього. Тоді як раз перед wait()методом, інший потік йде до setConditionToTrue()і встановлює x.conditionдо trueі notifyAll().
  2. Тоді лише після цього 1-а нитка ввійде в його wait()метод (не впливає на те, notifyAll()що сталося за кілька моментів раніше) У цьому випадку 1-й потік буде чекати, коли виконає інший потік setConditionToTrue(), але це може не повторитися.

Але якщо поставити synchronizedперед методами, які змінюють стан об’єкта, цього не станеться.

class A {

    private Object X;

    makeChangeOnX(){
        while (! x.getCondition()){
            wait();
            }
        // Do the change
    }

    setConditionToTrue(){
        x.condition = true; 
        notifyAll();

    }
    setConditionToFalse(){
        x.condition = false;
        notifyAll();
    }
    bool getCondition(){
        return x.condition;
    }
}

2

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

boolean wasNotified = false;
while(!wasNotified) {
    wait();
}

Тоді сповіщення наборів потоків wasNotified змінної to true та notify.

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

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

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

synchronized(monitor) {
    boolean wasNotified = false;
    while(!wasNotified) {
        wait();
    }
}

Дякую, сподіваємось, що це з’ясовується.


1

Це в основному пов'язане з апаратною архітектурою (тобто оперативною пам'яттю та кешами ).

Якщо ви не використовуєте synchronizedразом з wait()або notify(), інший потік може ввести цей же блок замість того, щоб чекати, коли монітор введе його. Більше того, коли, наприклад, звертається до масиву без синхронізованого блоку, інший потік може не бачити змін до нього ... насправді інший потік не побачить жодних змін до нього, коли він вже має копію масиву в кеш-рівні x ( також кеші 1-го / 2-го / 3-го рівня) ядра процесора, що обробляє потоки.

Але синхронізовані блоки - лише одна сторона медалі: Якщо ви фактично отримуєте доступ до об'єкта в синхронізованому контексті з несинхронізованого контексту, об’єкт все ще не буде синхронізований навіть у синхронізованому блоці, оскільки він містить власну копію об'єкта в його кеші. Я писав про ці проблеми тут: https://stackoverflow.com/a/21462631, і коли замок містить не завершальний об'єкт, чи може посилання на об'єкт все-таки змінити інший потік?

Крім того, я переконаний, що кеші рівня x відповідають за більшість помилок виконання, що не відтворюються. Це тому, що розробники зазвичай не вивчають матеріали низького рівня, наприклад, як працює процесор чи як ієрархія пам’яті впливає на роботу програм: http://en.wikipedia.org/wiki/Memory_hierarchy

Залишається загадкою, чому заняття програмування спочатку не починаються з ієрархії пам'яті та архітектури процесора. "Hello world" тут не допоможе. ;)


1
Щойно відкрили веб-сайт, який ідеально і глибоко пояснює це: javamex.com/tutorials/…
Маркус

Хм .. не впевнений, що я слідую. Якщо кешування було єдиною причиною розміщення очікування та повідомлення всередині синхронізованого, чому синхронізація не вкладається всередину реалізації функцій очікування / сповіщення?
aioobe

Хороше запитання, оскільки очікування / сповіщення цілком може бути синхронізованими методами ... можливо, колишні розробники Java Sun знають відповідь? Подивіться на засланні вище, або , можливо , це також допоможе вам: docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
Маркуса

Причиною може бути: У перші дні Java не було помилок компіляції під час виклику синхронізованого перед виконанням цих багатопотокових операцій. Натомість були лише помилки виконання (наприклад, coderanch.com/t/239491/java-programmer-SCJP/certification/… ). Можливо, вони подумали @SUN, що коли програмісти отримують ці помилки, з ними контактують, що, можливо, дало їм можливість продати більше своїх серверів. Коли це змінилося? Можливо Java 5.0 або 6.0, але насправді я не пам’ятаю, щоб бути чесним ...
Маркус

TBH Я бачу кілька питань з вашою відповіддю 1) Ваше друге речення не має сенсу: не має значення, на якому об'єкті нитка має замок. Незалежно від того, на якому об'єкті синхронізуються два потоки, всі зміни стають видимими. 2) Ви говорите, що інший потік "не" не побачить змін. Це має бути "не може" . 3) Я не знаю, чому ви створюєте кеші 1-го / 2-го / 3-го рівня ... Тут важливо, що говорить модель пам'яті Java і що вказано в JLS. Хоча архітектура апаратури може допомогти зрозуміти, чому JLS говорить, що вона робить, це в цьому контексті суворо кажучи не має значення.
aioobe

0

безпосередньо з цього підручника Java oracle:

Коли потік викликає d.wait, він повинен мати власний замок для d - інакше буде видано помилку. Виклик очікування всередині синхронізованого методу - це простий спосіб отримати внутрішній замок.


З запитання, яке висунув автор, не здається, що автор запитання має чітке розуміння того, що я цитував із підручника. Крім того, моя відповідь пояснює "Чому".
Rollerball

0

Коли ви викликаєте notify () від об'єкта t, java сповіщає про певний метод t.wait (). Але як ява шукає та повідомляє певний метод очікування.

java дивиться лише в синхронізований блок коду, який був заблокований об'єктом t. java не може шукати весь код, щоб повідомити певний t.wait ().


0

відповідно до документів:

Поточний потік повинен володіти монітором цього об’єкта. Нитка звільняє право власності на цей монітор.

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


0

Зачекайте нитку на об'єкті моніторингу (об'єкт, який використовується блоком синхронізації), може бути n кількість об'єктів моніторингу протягом усього шляху одного потоку. Якщо потік зачекає поза блоком синхронізації, то немає об'єкта моніторингу, а також інший потік сповістить про доступ до об'єкта моніторингу, тож як потік поза блоком синхронізації буде знати, що про нього було повідомлено. Це також одна з причин того, що wait (), notify () та notifyAll () є в об'єктному класі, а не в потоковому класі.

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

class A {
   int a = 0;
  //something......
  public void add() {
   synchronization(this) {
      //this is your monitoring object and thread has to wait to gain lock on **this**
       }
  }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.